diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..751002720 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +# Ignore bundler config. +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-journal + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/tmp/.keep +!/log/.keep + +# Puma is unhappy without this +!/tmp/pids +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore uploaded files in development +/storage/* +!/storage/.keep + +/node_modules +/yarn-error.log + +/public/assets +.byebug_history + +# Ignore master key for decrypting credentials and more. +/config/master.key + +# Ignore public uploads +public/uploads + +# Ignore changed bundle binstub +bin/bundle + +# Ignore local environment variables file +.env.local +.envrc + +# Ignore local gems +vendor/bundle/**/ + +.git/ +.github/ diff --git a/.github/workflows/build-manual.yaml b/.github/workflows/build-manual.yaml new file mode 100644 index 000000000..f45c692ac --- /dev/null +++ b/.github/workflows/build-manual.yaml @@ -0,0 +1,40 @@ +name: 'Manual build without tests' + +on: + workflow_dispatch: + inputs: + environment: + description: 'Environment to build in' + required: true + type: choice + options: + - pitc-ptime-int + - pitc-ptime-prod + - pude-ptime-prod + default: 'pitc-ptime-int' + project_name: + description: 'Project to build for' + required: true + type: choice + options: + - pitc-ptime-int + - pitc-ptime-prod + - pude-ptime-prod + default: 'pitc-ptime-int' + push: + description: 'Should we push the image to the registry?' + required: true + type: boolean + default: true + +jobs: + build-dispatch: + uses: ./.github/workflows/reusable-build.yaml + with: + environment: ${{ inputs.environment }} + project_name: ${{ inputs.project_name }} + push: ${{ inputs.push }} + registry: registry.puzzle.ch + secrets: + PUZZLE_REGISTRY_USERNAME: ${{ secrets.PUZZLE_REGISTRY_USERNAME }} + PUZZLE_REGISTRY_TOKEN: ${{ secrets.PUZZLE_REGISTRY_TOKEN }} diff --git a/.github/workflows/build-on-push.yaml b/.github/workflows/build-on-push.yaml new file mode 100644 index 000000000..0f3a8aa4e --- /dev/null +++ b/.github/workflows/build-on-push.yaml @@ -0,0 +1,82 @@ +name: 'Lint, test and image build on push' + +on: + push: + branches: + - master + paths-ignore: + - 'doc/**' + - '**.md' + +jobs: + lint: + uses: ./.github/workflows/reusable-lint.yaml + + test: + uses: ./.github/workflows/reusable-test.yaml + + build-int: + needs: [lint, test] + uses: ./.github/workflows/reusable-build.yaml + with: + environment: pitc-ptime-int + project_name: pitc-ptime-int + push: true + registry: registry.puzzle.ch + secrets: + PUZZLE_REGISTRY_USERNAME: ${{ secrets.PUZZLE_REGISTRY_USERNAME }} + PUZZLE_REGISTRY_TOKEN: ${{ secrets.PUZZLE_REGISTRY_TOKEN }} + + sbom-int: + needs: build-int + uses: ./.github/workflows/reusable-sbom.yaml + with: + project_name: pitc-ptime-int + artifact-prefix: int- + secrets: + dependency_track_url: ${{ secrets.DEPTRACK_URL }} + dependency_track_api_key: ${{ secrets.DEPTRACK_API_KEY }} + + build-prod: + needs: [lint, test] + uses: ./.github/workflows/reusable-build.yaml + with: + environment: pitc-ptime-prod + project_name: pitc-ptime-prod + push: true + registry: registry.puzzle.ch + secrets: + PUZZLE_REGISTRY_USERNAME: ${{ secrets.PUZZLE_REGISTRY_USERNAME }} + PUZZLE_REGISTRY_TOKEN: ${{ secrets.PUZZLE_REGISTRY_TOKEN }} + + sbom-prod: + needs: build-prod + uses: ./.github/workflows/reusable-sbom.yaml + with: + project_name: pitc-ptime-prod + artifact-prefix: prod- + secrets: + dependency_track_url: ${{ secrets.DEPTRACK_URL }} + dependency_track_api_key: ${{ secrets.DEPTRACK_API_KEY }} + + build-pude: + needs: [lint, test] + uses: ./.github/workflows/reusable-build.yaml + with: + environment: pude-ptime-prod + project_name: pude-ptime-prod + push: true + registry: registry.puzzle.ch + secrets: + PUZZLE_REGISTRY_USERNAME: ${{ secrets.PUZZLE_REGISTRY_USERNAME }} + PUZZLE_REGISTRY_TOKEN: ${{ secrets.PUZZLE_REGISTRY_TOKEN }} + + sbom-pude: + needs: build-pude + uses: ./.github/workflows/reusable-sbom.yaml + with: + project_name: pude-ptime-prod + artifact-prefix: pude- + secrets: + dependency_track_url: ${{ secrets.DEPTRACK_URL }} + dependency_track_api_key: ${{ secrets.DEPTRACK_API_KEY }} diff --git a/.github/workflows/code-style-review.yaml b/.github/workflows/code-style-review.yaml deleted file mode 100644 index 0f7b8ce6c..000000000 --- a/.github/workflows/code-style-review.yaml +++ /dev/null @@ -1,59 +0,0 @@ -name: 'Code Style Review' - -on: - pull_request: - branches: - - 'master' - paths-ignore: - - 'doc/**' - - '**.md' - -jobs: - lint: - runs-on: 'ubuntu-latest' - - steps: - - name: 'Checkout' - uses: actions/checkout@v2 - - - name: 'Set up Ruby' - uses: actions/setup-ruby@v1 - with: - ruby-version: '2.6' - - - name: 'Set up Reviewdog' - env: - REVIEWDOG_INSTALLER: 'https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh' - run: | - mkdir -p $HOME/bin - curl -sfL ${REVIEWDOG_INSTALLER} | sh -s -- -b $HOME/bin - - - name: 'Set up Rubocop' - run: 'gem install rubocop rubocop-minitest rubocop-performance rubocop-rails' - - - name: 'Run Rubocop' - env: - REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - rubocop | - $HOME/bin/reviewdog \ - -f=rubocop \ - -name="Rubocop" \ - -reporter=github-pr-review \ - -level=error \ - -diff="git diff $DIFF_BRANCH" - - - name: 'Set up Haml-lint' - run: 'gem install haml-lint' - - - name: 'Run Haml-lint' - env: - REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - haml-lint | - $HOME/bin/reviewdog \ - -efm="%f:%l %m" \ - -name="Haml-lint" \ - -reporter=github-pr-review \ - -level=error \ - -diff="git diff $DIFF_BRANCH" diff --git a/.github/workflows/reusable-build.yaml b/.github/workflows/reusable-build.yaml new file mode 100644 index 000000000..48888a618 --- /dev/null +++ b/.github/workflows/reusable-build.yaml @@ -0,0 +1,55 @@ +name: 'Reusable: Build and push image' + +on: + workflow_call: + inputs: + environment: + description: "Environment to run in" + type: string + required: true + project_name: + description: "Project name, e.g. pitc-ptime-int" + type: string + required: true + push: + description: "Build and debug things without pushing to the registry" + type: boolean + default: false + registry: + description: 'FQDN of the registry' + type: string + required: true + + secrets: + PUZZLE_REGISTRY_USERNAME: + description: 'Needed for the registry login' + required: true + PUZZLE_REGISTRY_TOKEN: + description: 'Needed for the registry login' + required: true + +jobs: + build: + environment: ${{ inputs.environment }} + runs-on: 'ubuntu-latest' + steps: + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ secrets.PUZZLE_REGISTRY_USERNAME }} + password: ${{ secrets.PUZZLE_REGISTRY_TOKEN }} + - uses: docker/build-push-action@v4 + with: + file: Dockerfile + build-args: | + BUILD_REPO=$GITHUB_REPOSITORY + BUILD_REF=$GITHUB_REF_NAME + BUILD_COMMIT=$GITHUB_SHA + target: app + tags: | + ${{ inputs.registry }}/ptime/${{ inputs.project_name }}:latest + ${{ inputs.registry }}/ptime/${{ inputs.project_name }}:${{ github.sha }} + push: ${{ inputs.push }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/reusable-lint.yaml b/.github/workflows/reusable-lint.yaml new file mode 100644 index 000000000..58b039837 --- /dev/null +++ b/.github/workflows/reusable-lint.yaml @@ -0,0 +1,38 @@ +name: 'Code Style Review' + +on: + workflow_call: + +jobs: + lint: + runs-on: 'ubuntu-latest' + + steps: + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - uses: reviewdog/action-rubocop@v2 + with: + rubocop_version: gemfile + rubocop_extensions: rubocop-minitest:gemfile rubocop-performance:gemfile rubocop-rails:gemfile + reporter: github-pr-review + level: error + + - run: 'gem install haml-lint' + + - uses: reviewdog/action-setup@v1 + + - name: 'Run Reviewdog HAML-Lint' + env: + REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }} + run: | + haml-lint | + reviewdog \ + -efm="%f:%l %m" \ + -name="HAML-Lint" \ + -reporter=github-pr-review \ + -level=error \ + -diff="git diff $DIFF_BRANCH" diff --git a/.github/workflows/reusable-sbom.yaml b/.github/workflows/reusable-sbom.yaml new file mode 100644 index 000000000..dff784b85 --- /dev/null +++ b/.github/workflows/reusable-sbom.yaml @@ -0,0 +1,88 @@ +name: Generate and push an SBOM dependency list + +on: + workflow_call: + inputs: + project_name: + type: string + description: "Project name to report to dependency track" + required: true + artifact-prefix: + type: string + description: "what to prefix the artifacts with e.g. int-" + default: "" + + secrets: + dependency_track_url: + description: "Base URL of the dependency track instance" + required: true + dependency_track_api_key: + description: "API key to use for sending data to dependency track" + required: true + +jobs: + generate-and-push-sbom: + runs-on: 'ubuntu-latest' + env: + HEADLESS: true + RAILS_TEST_DB_NAME: 'postgres' + RAILS_TEST_DB_USERNAME: 'postgres' + RAILS_TEST_DB_PASSWORD: 'postgres' + RAILS_ENV: 'test' + CI: 'true' + PGDATESTYLE: German + + services: + postgres: + image: 'postgres:11' + env: + POSTGRES_PASSWORD: 'postgres' + ports: [ '5432:5432' ] + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 10s + --health-retries 10 + memcached: + image: 'memcached' + ports: [ '11211:11211' ] + + steps: + - uses: actions/checkout@v4 + - name: 'Generate SBOM for ruby and npm dependencies' + run: | + npm install -g @cyclonedx/cdxgen + cdxgen -o ./sbom-ruby.json -t ruby --spec-version 1.4 . + cdxgen -o ./sbom-npm.json -t npm --spec-version 1.4 . + + - name: 'Merge frontend and backend SBOMs' + run: | + docker run \ + --rm \ + -v $(pwd):/data \ + cyclonedx/cyclonedx-cli \ + merge \ + --input-files data/sbom-ruby.json data/sbom-npm.json \ + --output-file data/sbom.xml + + - name: 'Push merged SBOM to dependency track' + run: | + curl --verbose \ + -s \ + --location \ + --request POST ${{ secrets.dependency_track_url }}/api/v1/bom \ + --header "X-Api-Key: ${{ secrets.dependency_track_api_key }}" \ + --header "Content-Type: multipart/form-data" \ + --form "autoCreate=true" \ + --form "projectName=${{ inputs.project_name }}" \ + --form "projectVersion=latest" \ + --form "bom=@sbom.xml" + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ inputs.artifact-prefix }}sboms + path: | + ./sbom-npm.json + ./sbom-ruby.json + ./sbom.xml diff --git a/.github/workflows/rails-tests.yaml b/.github/workflows/reusable-test.yaml similarity index 50% rename from .github/workflows/rails-tests.yaml rename to .github/workflows/reusable-test.yaml index 730a521cf..218bf3b81 100644 --- a/.github/workflows/rails-tests.yaml +++ b/.github/workflows/reusable-test.yaml @@ -1,53 +1,34 @@ -name: 'Rails Unit Tests' +name: "Rails Unit Tests" on: - push: - branches: - - 'master' - paths-ignore: - - 'doc/**' - - '**.md' - pull_request: - branches: - - 'master' - paths-ignore: - - 'doc/**' - - '**.md' + workflow_call: jobs: - build: + minitest: runs-on: 'ubuntu-latest' - strategy: - matrix: - ruby: [ '2.5', '2.6', '2.7' ] + timeout-minutes: 15 services: postgres: - image: 'postgres:11' + image: "postgres:11" env: - POSTGRES_PASSWORD: 'postgres' - ports: [ '5432:5432' ] + POSTGRES_PASSWORD: "postgres" + ports: ["5432:5432"] options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 10s --health-retries 10 memcached: - image: 'memcached' - ports: [ '11211:11211' ] + image: "memcached" + ports: ["11211:11211"] steps: - - name: 'Checkout' - uses: actions/checkout@v2 - - - name: 'Set up Ruby ${{ matrix.ruby }}' - uses: ruby/setup-ruby@v1.59.1 + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 with: - ruby-version: ${{ matrix.ruby }} bundler-cache: true - env: - ImageOS: ubuntu18 - name: 'Build and test with Rake' env: @@ -55,10 +36,13 @@ jobs: RAILS_TEST_DB_USERNAME: 'postgres' RAILS_TEST_DB_PASSWORD: 'postgres' RAILS_ENV: 'test' + CI: 'true' + PGDATESTYLE: German run: | - sudo apt-get -yqq install libpq-dev + sudo apt-get -yqq install libpq-dev libvips-dev gem install bundler --version '~> 2' bundle install --jobs 4 --retry 3 bundle exec rails db:create bundle exec rails db:migrate + bundle exec rails assets:precompile bundle exec rails test diff --git a/.github/workflows/test-on-pr.yaml b/.github/workflows/test-on-pr.yaml new file mode 100644 index 000000000..358aabcfd --- /dev/null +++ b/.github/workflows/test-on-pr.yaml @@ -0,0 +1,15 @@ +name: 'Lint and test on pull request' + +on: + pull_request_target: + branches: + - 'master' + paths-ignore: + - 'doc/**' + - '**.md' + +jobs: + lint: + uses: ./.github/workflows/reusable-lint.yaml + test: + uses: ./.github/workflows/reusable-test.yaml diff --git a/.gitignore b/.gitignore index baf4cefc8..3181e8069 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ .idea/ *.iml /.byebug_history +.vscode/ # Ignore the default SQLite database. /db/*.sqlite3 @@ -21,6 +22,7 @@ # Ignore all logfiles and tempfiles. /log/*.log /tmp +!/tmp/pids test/coverage test/reports @@ -55,3 +57,8 @@ tags # bundler tools dir /vendor/tools + +# Ignore public assets +/public/assets + +TAGS diff --git a/.rubocop.yml b/.rubocop.yml index b8fd9c8bb..f96f0f394 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -9,16 +9,27 @@ require: AllCops: NewCops: enable -Style/HashEachMethods: - Enabled: true +Naming/InclusiveLanguage: + Exclude: + - config/environments/development.rb # Bullet uses whitelist :( -Style/HashTransformKeys: - Enabled: true +# Not gonna rewrite all our tests... +Minitest/MultipleAssertions: + Enabled: false -Style/HashTransformValues: - Enabled: true +Rails/ActionControllerTestCase: + Enabled: false -Naming/InclusiveLanguage: +# Ignore for existing migrations +Rails/CreateTableWithTimestamps: Exclude: - - config/environments/development.rb # Bullet uses whitelist :( + - 'db/migrate/20150930143851_create_tables.rb' + - 'db/migrate/20151105101420_create_sectors_and_services.rb' + - 'db/migrate/20160919101807_add_daily_plannings.rb' + - 'db/migrate/20160929150008_create_custom_lists.rb' + - 'db/migrate/20170321095600_employee_master_data.rb' + - 'db/migrate/20170724095653_create_additional_crm_orders.rb' + - 'db/migrate/20190204150125_add_expenses.rb' + - 'db/migrate/20220322152042_create_workplaces.rb' + - 'db/migrate/20230207231358_create_active_storage_variant_records.active_storage.rb' ... diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e71501dff..0341abffe 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,74 +1,46 @@ ---- # This configuration was generated by # `rubocop --auto-gen-config` -# on 2020-03-05 15:02:28 +0100 using RuboCop version 0.80.0. +# on 2024-02-07 22:19:57 UTC using RuboCop version 1.60.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 151 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: leading, trailing -Layout/DotPosition: - Enabled: false - -# Offense count: 97 -# Cop supports --auto-correct. -# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. -# SupportedHashRocketStyles: key, separator, table -# SupportedColonStyles: key, separator, table -# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit -Layout/HashAlignment: - Enabled: false - -# Offense count: 7 -Lint/AmbiguousBlockAssociation: - Exclude: - - 'test/controllers/orders_controller_test.rb' - - 'test/domain/plannings/creator_test.rb' - - 'test/domain/plannings/order_board_test.rb' - - 'test/integration/invoice_form_test.rb' - -# Offense count: 4 -Lint/AmbiguousOperator: +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. +# SupportedStyles: space, no_space, compact +# SupportedStylesForEmptyBrackets: space, no_space +Layout/SpaceInsideArrayLiteralBrackets: Exclude: - - 'test/domain/order/cockpit_test.rb' + - 'app/models/employment.rb' -# Offense count: 55 -Lint/AmbiguousRegexpLiteral: +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowInHeredoc. +Layout/TrailingWhitespace: Exclude: - - 'test/controllers/crud_test_models_controller_test.rb' - - 'test/controllers/employees/worktimes_commit_controller_test.rb' - - 'test/controllers/evaluator_controller_test.rb' - - 'test/controllers/order_reports_controller_test.rb' - - 'test/controllers/order_services_controller_test.rb' - - 'test/controllers/orders/committed_controller_test.rb' - - 'test/controllers/orders/completed_controller_test.rb' - - 'test/domain/order/report_test.rb' - - 'test/helpers/dry_crud/form/builder_test.rb' - - 'test/helpers/form_helper_test.rb' - - 'test/helpers/table_helper_test.rb' - - 'test/integration/create_order_test.rb' + - 'app/helpers/navigation_helper.rb' + - 'app/models/employment.rb' + - 'app/models/user_notification.rb' -# Offense count: 1 -# Configuration parameters: AllowSafeAssignment. -Lint/AssignmentInCondition: +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +Lint/AmbiguousOperator: Exclude: - - 'app/models/worktime.rb' + - 'test/models/employee_test.rb' # Offense count: 1 -# Cop supports --auto-correct. -Lint/BigDecimalNew: +Lint/BinaryOperatorWithIdenticalOperands: Exclude: - - 'config/initializers/nil_to_d.rb' + - 'test/models/util/period_test.rb' -# Offense count: 1 -# Cop supports --auto-correct. -Lint/DeprecatedClassMethods: +# Offense count: 3 +# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. +Lint/DuplicateBranch: Exclude: - - 'config/application.rb' + - 'app/controllers/concerns/worktimes_report.rb' + - 'app/domain/plannings/employee_board.rb' # Offense count: 4 Lint/DuplicateMethods: @@ -77,10 +49,11 @@ Lint/DuplicateMethods: - 'app/domain/order/copier.rb' - 'app/helpers/dry_crud/form/control.rb' -# Offense count: 2 -Lint/IneffectiveAccessModifier: +# Offense count: 1 +# Configuration parameters: AllowComments, AllowEmptyLambdas. +Lint/EmptyBlock: Exclude: - - 'config/application.rb' + - 'test/helpers/table_helper_test.rb' # Offense count: 2 Lint/LiteralAsCondition: @@ -88,15 +61,21 @@ Lint/LiteralAsCondition: - 'test/helpers/dry_crud/form/builder_test.rb' # Offense count: 1 +# Configuration parameters: AllowedParentClasses. +Lint/MissingSuper: + Exclude: + - 'app/serializers/api/api_serializer.rb' + +# Offense count: 1 +# Configuration parameters: AllowedMethods, AllowedPatterns. Lint/NestedMethodDefinition: Exclude: - 'test/helpers/table_helper_test.rb' # Offense count: 1 -# Cop supports --auto-correct. -Lint/RedundantCopEnableDirective: +Lint/ShadowingOuterLocalVariable: Exclude: - - 'app/helpers/dry_crud/form/builder.rb' + - 'app/domain/invoicing/small_invoice/api.rb' # Offense count: 1 # Configuration parameters: AllowKeywordBlockArguments. @@ -104,171 +83,91 @@ Lint/UnderscorePrefixedVariableName: Exclude: - 'test/support/crud_test_helper.rb' -# Offense count: 1 +# Offense count: 2 Lint/UnreachableCode: Exclude: + - 'app/presenters/employees/vcard.rb' - 'test/integration/plannings_orders_test.rb' # Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. -Lint/UnusedBlockArgument: +# Configuration parameters: AllowedPatterns. +# AllowedPatterns: (?-mix:(exactly|at_least|at_most)\(\d+\)\.times) +Lint/UnreachableLoop: Exclude: - - 'test/fabricators/planning_fabricator.rb' - -# Offense count: 2 -# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. -Lint/UselessAccessModifier: - Exclude: - - 'config/application.rb' - - 'test/support/crud_test_models_controller.rb' - -# Offense count: 19 -Lint/UselessAssignment: - Exclude: - - 'app/models/accounting_post.rb' - - 'test/controllers/absencetimes_controller_test.rb' - - 'test/controllers/plannings/multi_employees_controller_test.rb' - - 'test/controllers/plannings/multi_orders_controller_test.rb' - - 'test/domain/order/report_test.rb' - - 'test/domain/plannings/creator_test.rb' - - 'test/domain/plannings/order_board_test.rb' - - 'test/integration/create_order_test.rb' - - 'test/models/employment_test.rb' - - 'test/models/order_test.rb' - - 'test/models/work_item_test.rb' - - 'test/models/worktime_test.rb' + - 'app/models/working_condition.rb' -# Offense count: 213 +# Offense count: 161 +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: - Max: 280 + Max: 282 -# Offense count: 47 -# Configuration parameters: CountComments, ExcludedMethods. -# ExcludedMethods: refine +# Offense count: 51 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +# AllowedMethods: refine Metrics/BlockLength: - Max: 172 + Max: 170 -# Offense count: 1 -# Configuration parameters: CountBlocks. -Metrics/BlockNesting: - Max: 4 - -# Offense count: 78 -# Configuration parameters: CountComments. +# Offense count: 82 +# Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 493 + Max: 504 -# Offense count: 17 +# Offense count: 18 +# Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: - Max: 10 + Max: 13 -# Offense count: 187 -# Configuration parameters: CountComments, ExcludedMethods. +# Offense count: 181 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: - Max: 311 + Max: 312 -# Offense count: 7 -# Configuration parameters: CountComments. +# Offense count: 8 +# Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Max: 204 + Max: 205 -# Offense count: 1 +# Offense count: 2 # Configuration parameters: CountKeywordArgs. Metrics/ParameterLists: Max: 6 + MaxOptionalParameters: 4 -# Offense count: 8 +# Offense count: 7 +# Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: - Max: 10 + Max: 13 -# Offense count: 1 -# Cop supports --auto-correct. -Migration/DepartmentName: - Exclude: - - 'app/helpers/dry_crud/form/builder.rb' - -# Offense count: 21 -# Cop supports --auto-correct. -Minitest/AssertEqual: +# Offense count: 7 +# Configuration parameters: Severity. +Minitest/AssertWithExpectedArgument: Exclude: - 'test/controllers/invoices_controller_test.rb' - - 'test/domain/plannings/item_test.rb' - - 'test/support/custom_assertions.rb' - -# Offense count: 34 -# Cop supports --auto-correct. -Minitest/AssertIncludes: - Exclude: - - 'test/controllers/employments_controller_test.rb' - - 'test/controllers/order_comments_controller_test.rb' - - 'test/controllers/plannings/employees_controller_test.rb' - - 'test/controllers/plannings/multi_employees_controller_test.rb' - - 'test/controllers/plannings/multi_orders_controller_test.rb' - - 'test/controllers/plannings/orders_controller_test.rb' - - 'test/domain/plannings/creator_test.rb' - - 'test/helpers/dry_crud/form/builder_test.rb' - - 'test/integration/create_order_test.rb' - - 'test/integration/edit_order_test.rb' - - 'test/integration/plannings_orders_test.rb' - - 'test/support/crud_controller_test_helper.rb' - - 'test/support/custom_assertions.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Minitest/AssertRespondTo: - Exclude: - - 'test/helpers/dry_crud/form/builder_test.rb' - -# Offense count: 22 -# Cop supports --auto-correct. -Minitest/AssertTruthy: - Exclude: - - 'test/controllers/accounting_posts_controller_test.rb' - - 'test/controllers/multi_worktimes_controller_test.rb' - - 'test/controllers/order_reports_controller_test.rb' - - 'test/controllers/workload_report_controller_test.rb' - - 'test/domain/order/copier_test.rb' - - 'test/models/accounting_post_test.rb' + - 'test/controllers/order_uncertainties_controller_test.rb' + - 'test/models/employee_test.rb' + - 'test/models/invoice_test.rb' - 'test/models/work_item_test.rb' + - 'test/support/crud_controller_test_helper.rb' -# Offense count: 72 -# Cop supports --auto-correct. -Minitest/RefuteFalse: - Enabled: false - -# Offense count: 4 -# Cop supports --auto-correct. -Minitest/RefuteIncludes: - Exclude: - - 'test/domain/plannings/creator_test.rb' - -# Offense count: 7 -Naming/AccessorMethodName: +# Offense count: 1 +Minitest/TestFileName: Exclude: - - 'app/controllers/absencetimes_controller.rb' - - 'app/domain/evaluations/clients_eval.rb' - - 'app/domain/evaluations/employee_work_items_eval.rb' - - 'app/domain/evaluations/evaluation.rb' - - 'app/domain/evaluations/work_item_employees_eval.rb' - - 'app/domain/invoicing/small_invoice/client_sync.rb' - - 'test/controllers/order_comments_controller_test.rb' + - 'test/integration/edit_worktimes_as_order_responsible.rb' # Offense count: 1 -# Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms. -# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS -Naming/FileName: +Naming/AccessorMethodName: Exclude: - - 'config/initializers/rswag-ui.rb' + - 'app/domain/invoicing/small_invoice/api.rb' # Offense count: 1 # Configuration parameters: ForbiddenDelimiters. -# ForbiddenDelimiters: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$)) +# ForbiddenDelimiters: (?i-mx:(^|\s)(EO[A-Z]{1}|END)(\s|$)) Naming/HeredocDelimiterNaming: Exclude: - 'lib/tasks/license.rake' # Offense count: 14 +# This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: @@ -288,7 +187,7 @@ Naming/MemoizedInstanceVariableName: # Offense count: 26 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. -# AllowedNames: io, id, to, by, on, in, at, ip, db, os, pp +# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to Naming/MethodParameterName: Exclude: - 'app/domain/invoicing/small_invoice/invoice_sync.rb' @@ -315,98 +214,52 @@ Naming/MethodParameterName: # MethodDefinitionMacros: define_method, define_singleton_method Naming/PredicateName: Exclude: - - 'spec/**/*' - 'app/helpers/dry_crud/form/builder.rb' - 'app/models/concerns/belonging_to_work_item.rb' -# Offense count: 7 -# Cop supports --auto-correct. -# Configuration parameters: PreferredName. -Naming/RescuedExceptionsVariableName: - Exclude: - - 'app/controllers/concerns/with_period.rb' - - 'app/controllers/periods_controller.rb' - - 'app/domain/crm/highrise.rb' - - 'app/domain/invoicing/small_invoice/client_sync.rb' - - 'app/domain/invoicing/small_invoice/invoice_sync.rb' - - 'app/models/accounting_post.rb' - # Offense count: 5 -# Configuration parameters: EnforcedStyle. +# Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns. # SupportedStyles: snake_case, camelCase Naming/VariableName: Exclude: - 'test/models/worktime_test.rb' -# Offense count: 1 -Performance/FixedSize: - Exclude: - - 'test/support/crud_controller_test_helper.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Performance/InefficientHashSearch: - Exclude: - - 'app/helpers/order_helper.rb' - - 'app/models/holiday.rb' - -# Offense count: 6 -# Cop supports --auto-correct. -Performance/RegexpMatch: - Exclude: - - 'app/controllers/concerns/csv_exportable.rb' - - 'app/controllers/evaluator_controller.rb' - - 'app/domain/apidocs/tags_setup.rb' - - 'app/models/employee.rb' - - 'lib/generators/error_page_generator.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect. -Performance/TimesMap: +# Offense count: 15 +# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. +# SupportedStyles: snake_case, normalcase, non_integer +# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 +Naming/VariableNumber: Exclude: + - 'test/controllers/contacts_controller_test.rb' + - 'test/controllers/employments_controller_test.rb' + - 'test/controllers/invoices_controller_test.rb' + - 'test/controllers/orders_controller_test.rb' + - 'test/controllers/overtime_vacations_controller_test.rb' + - 'test/integration/create_order_test.rb' + - 'test/models/employment_test.rb' - 'test/models/invoice_test.rb' + - 'test/models/order_test.rb' -# Offense count: 12 -# Cop supports --auto-correct. -Rails/ActiveRecordAliases: +# Offense count: 1 +# Configuration parameters: MinSize. +Performance/CollectionLiteralInLoop: Exclude: - - 'app/controllers/employees_controller.rb' - - 'app/models/employee.rb' - - 'test/controllers/absencetimes_controller_test.rb' - - 'test/controllers/employees/log_controller_test.rb' - - 'test/controllers/ordertimes_controller_test.rb' - - 'test/models/work_item_test.rb' + - 'test/controllers/evaluator_controller_test.rb' # Offense count: 1 -# Cop supports --auto-correct. -Rails/ApplicationMailer: +Performance/FixedSize: Exclude: - - 'app/mailers/employee_mailer.rb' - -# Offense count: 39 -# Cop supports --auto-correct. -Rails/ApplicationRecord: - Enabled: false - -# Offense count: 60 -# Cop supports --auto-correct. -# Configuration parameters: Include. -# Include: **/test/**/* -Rails/AssertNot: - Enabled: false + - 'test/support/crud_controller_test_helper.rb' -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: NilOrEmpty, NotPresent, UnlessPresent. -Rails/Blank: +# Offense count: 3 +Performance/MethodObjectAsBlock: Exclude: - - 'app/helpers/employee_master_data_helper.rb' - - 'config/initializers/country_select.rb' + - 'app/domain/apidocs/setup.rb' + - 'app/helpers/order_helper.rb' -# Offense count: 34 +# Offense count: 32 # Configuration parameters: Include. -# Include: db/migrate/*.rb +# Include: db/**/*.rb Rails/CreateTableWithTimestamps: Exclude: - 'db/migrate/20150930143851_create_tables.rb' @@ -416,69 +269,15 @@ Rails/CreateTableWithTimestamps: - 'db/migrate/20170321095600_employee_master_data.rb' - 'db/migrate/20170724095653_create_additional_crm_orders.rb' - 'db/migrate/20190204150125_add_expenses.rb' + - 'db/migrate/20220322152042_create_workplaces.rb' -# Offense count: 39 -# Configuration parameters: EnforcedStyle. +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle, AllowToTime. # SupportedStyles: strict, flexible Rails/Date: Exclude: - - 'app/domain/order/cockpit/accounting_post_row.rb' - - 'app/helpers/employee_master_data_helper.rb' - - 'db/migrate/20160919101807_add_daily_plannings.rb' - - 'lib/tasks/license.rake' - - 'test/controllers/plannings/employees_controller_test.rb' - - 'test/controllers/plannings/multi_employees_controller_test.rb' - - 'test/controllers/plannings/multi_orders_controller_test.rb' - - 'test/controllers/plannings/orders_controller_test.rb' - - 'test/controllers/workload_report_controller_test.rb' - - 'test/controllers/worktimes_controller_test.rb' - - 'test/domain/order/cockpit_test.rb' - - 'test/models/expense_test.rb' - -# Offense count: 7 -# Cop supports --auto-correct. -# Configuration parameters: EnforceForPrefixed. -Rails/Delegate: - Exclude: - - 'app/domain/forms/splitable.rb' - - 'app/domain/order/cockpit/accounting_post_row.rb' - - 'app/domain/order/report.rb' - - 'app/domain/reports/workload.rb' - - 'app/domain/reports/workload/ordertime_entry.rb' - - 'app/models/company.rb' - -# Offense count: 63 -# Cop supports --auto-correct. -# Configuration parameters: Whitelist. -# Whitelist: find_by_sql -Rails/DynamicFindBy: - Exclude: - - 'db/seeds/development/zz_work_item.rb' - - 'test/controllers/absencetimes_controller_test.rb' - - 'test/controllers/crud_test_models_controller_test.rb' - - 'test/controllers/ordertimes_controller_test.rb' - - 'test/integration/create_order_test.rb' - - 'test/support/crud_test_helper.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/EnumHash: - Exclude: - - 'app/models/expense.rb' - - 'app/models/invoice.rb' - -# Offense count: 6 -# Configuration parameters: EnforcedStyle. -# SupportedStyles: slashes, arguments -Rails/FilePath: - Exclude: - - 'app/domain/apidocs/tags_setup.rb' - - 'config/application.rb' - - 'config/initializers/assets.rb' - - 'config/initializers/rswag-ui.rb' - - 'db/seeds.rb' + - 'test/models/employee_test.rb' # Offense count: 3 # Configuration parameters: Include. @@ -488,7 +287,7 @@ Rails/HasAndBelongsToMany: - 'app/models/employee.rb' - 'app/models/invoice.rb' -# Offense count: 11 +# Offense count: 14 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/HasManyOrHasOneDependent: @@ -503,14 +302,14 @@ Rails/HasManyOrHasOneDependent: - 'app/models/order_status.rb' - 'app/models/portfolio_item.rb' - 'app/models/service.rb' + - 'app/models/work_item.rb' -# Offense count: 79 +# Offense count: 55 # Configuration parameters: Include. # Include: app/helpers/**/*.rb Rails/HelperInstanceVariable: Exclude: - 'app/helpers/accounting_posts_helper.rb' - - 'app/helpers/dry_crud/form/builder.rb' - 'app/helpers/dry_crud/form/control.rb' - 'app/helpers/dry_crud/table/builder.rb' - 'app/helpers/evaluator_helper.rb' @@ -523,8 +322,23 @@ Rails/HelperInstanceVariable: - 'app/helpers/plannings_helper.rb' - 'app/helpers/worktime_helper.rb' +# Offense count: 14 +Rails/I18nLocaleTexts: + Exclude: + - 'app/controllers/application_controller.rb' + - 'app/controllers/capacity_report_controller.rb' + - 'app/controllers/employees_controller.rb' + - 'app/controllers/expenses_controller.rb' + - 'app/controllers/export_report_controller.rb' + - 'app/controllers/multi_worktimes_controller.rb' + - 'app/controllers/ordertimes_controller.rb' + - 'app/mailers/employee_mailer.rb' + - 'app/models/holiday.rb' + - 'app/models/overtime_vacation.rb' + - 'test/support/crud_test_models_controller.rb' + # Offense count: 10 -# Configuration parameters: Include. +# Configuration parameters: IgnoreScopes, Include. # Include: app/models/**/*.rb Rails/InverseOf: Exclude: @@ -537,7 +351,7 @@ Rails/InverseOf: # Offense count: 5 # Configuration parameters: Include. -# Include: app/controllers/**/*.rb +# Include: app/controllers/**/*.rb, app/mailers/**/*.rb Rails/LexicallyScopedActionFilter: Exclude: - 'app/controllers/dry_crud/rememberable.rb' @@ -546,259 +360,150 @@ Rails/LexicallyScopedActionFilter: - 'app/controllers/plannings/employees_controller.rb' - 'app/controllers/plannings/orders_controller.rb' -# Offense count: 14 +# Offense count: 13 Rails/OutputSafety: Exclude: - 'app/controllers/worktimes_controller.rb' - 'app/domain/graphs/vacation_graph.rb' - 'app/helpers/dry_crud/form/builder.rb' - 'app/helpers/evaluator_helper.rb' + - 'app/helpers/expenses_helper.rb' - 'app/helpers/form_helper.rb' - 'app/helpers/order_controlling_helper.rb' - 'app/helpers/sort_helper.rb' - 'app/helpers/worktime_helper.rb' - 'app/models/util/report_type.rb' -# Offense count: 19 -# Cop supports --auto-correct. -Rails/PluralizationGrammar: - Exclude: - - 'test/controllers/plannings/employees_controller_test.rb' - - 'test/controllers/plannings/multi_employees_controller_test.rb' - - 'test/controllers/plannings/multi_orders_controller_test.rb' - - 'test/controllers/plannings/orders_controller_test.rb' - - 'test/domain/plannings/employee_board_test.rb' - - 'test/domain/plannings/order_board_test.rb' - - 'test/integration/plannings_orders_test.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Rails/Presence: - Exclude: - - 'app/controllers/order_services_controller.rb' - - 'app/controllers/revenue_reports_controller.rb' - -# Offense count: 6 -# Configuration parameters: Include. -# Include: **/Rakefile, **/*.rake -Rails/RakeEnvironment: - Exclude: - - 'lib/tasks/analyze.rake' - - 'lib/tasks/license.rake' - - 'lib/tasks/model.rake' - -# Offense count: 51 -# Cop supports --auto-correct. -# Configuration parameters: Include. +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, Include. +# SupportedStyles: assert_not, refute # Include: **/test/**/* Rails/RefuteMethods: Exclude: - - 'test/controllers/employee_master_data_controller_test.rb' - - 'test/controllers/expenses_controller_test.rb' - - 'test/controllers/orders_controller_test.rb' - - 'test/controllers/plannings/employees_controller_test.rb' - - 'test/controllers/plannings/orders_controller_test.rb' - - 'test/domain/plannings/creator_test.rb' - - 'test/integration/invoice_form_test.rb' - - 'test/integration/show_order_services_test.rb' - - 'test/models/accounting_post_test.rb' - 'test/models/employee_test.rb' - - 'test/models/expense_test.rb' - - 'test/models/invoice_test.rb' - - 'test/models/ordertime_test.rb' - - 'test/models/work_item_test.rb' - - 'test/support/custom_assertions.rb' # Offense count: 2 # Configuration parameters: Include. -# Include: db/migrate/*.rb +# Include: db/**/*.rb Rails/ReversibleMigration: Exclude: - 'db/migrate/20160919101807_add_daily_plannings.rb' - 'db/migrate/20161222162235_move_probation_period_end_date_to_employee.rb' -# Offense count: 38 -# Configuration parameters: Blacklist, Whitelist. -# Blacklist: decrement!, decrement_counter, increment!, increment_counter, toggle!, touch, update_all, update_attribute, update_column, update_columns, update_counters +# Offense count: 62 +# Configuration parameters: ForbiddenMethods, AllowedMethods. +# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all Rails/SkipsModelValidations: Enabled: false -# Offense count: 6 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: strict, flexible -Rails/TimeZone: +# Offense count: 8 +# Configuration parameters: Include. +# Include: db/**/*.rb +Rails/ThreeStateBooleanColumn: Exclude: - - 'test/controllers/workload_report_controller_test.rb' - - 'test/models/worktime_test.rb' + - 'db/migrate/20150930143851_create_tables.rb' + - 'db/migrate/20170321095600_employee_master_data.rb' + +# Offense count: 1 +# Configuration parameters: TransactionMethods. +Rails/TransactionExitStatement: + Exclude: + - 'app/domain/plannings/creator.rb' + +# Offense count: 17 +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/UniqueValidationWithoutIndex: + Exclude: + - 'app/models/accounting_post.rb' + - 'app/models/billing_address.rb' + - 'app/models/client.rb' + - 'app/models/contact.rb' + - 'app/models/employee.rb' + - 'app/models/invoice.rb' + - 'app/models/order.rb' + - 'app/models/order_target.rb' + - 'app/models/sector.rb' + - 'app/models/service.rb' + - 'app/models/work_item.rb' + - 'app/models/working_condition.rb' + - 'app/models/workplace.rb' + +# Offense count: 1 +# Configuration parameters: Severity. +Rails/WhereNotWithMultipleConditions: + Exclude: + - 'app/domain/invoicing/small_invoice/invoice_sync.rb' + +# Offense count: 1 +Security/CompoundHash: + Exclude: + - 'app/models/util/period.rb' # Offense count: 1 Security/Eval: Exclude: - 'test/support/custom_assertions.rb' -# Offense count: 4 -# Cop supports --auto-correct. -Security/YAMLLoad: +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: percent_q, bare_percent +Style/BarePercentLiterals: Exclude: - - 'config/environments/development.rb' - - 'config/environments/production.rb' - -# Offense count: 45 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle. -# SupportedStyles: nested, compact -Style/ClassAndModuleChildren: - Enabled: false + - 'app/controllers/concerns/worktimes_report.rb' # Offense count: 2 Style/ClassVars: Exclude: - 'config/application.rb' -# Offense count: 2 -# Cop supports --auto-correct. +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. # SupportedStyles: assign_to_condition, assign_inside_condition Style/ConditionalAssignment: Exclude: - - 'app/controllers/evaluator_controller.rb' - - 'test/helpers/dry_crud/form/builder_test.rb' + - 'app/controllers/concerns/worktimes_report.rb' -# Offense count: 285 +# Offense count: 306 +# Configuration parameters: AllowedConstants. Style/Documentation: Enabled: false -# Offense count: 25 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: compact, expanded -Style/EmptyMethod: - Exclude: - - 'app/controllers/configurations_controller.rb' - - 'app/controllers/crud_controller.rb' - - 'app/controllers/employees_controller.rb' - - 'app/controllers/order_targets_controller.rb' - - 'app/controllers/worktimes_controller.rb' - - 'app/domain/crm/base.rb' - - 'app/domain/invoicing/interface.rb' - - 'app/domain/order/report/total.rb' - - 'app/models/ability.rb' - - 'app/models/worktime.rb' - -# Offense count: 12 -# Cop supports --auto-correct. -Style/Encoding: - Exclude: - - 'app/controllers/capacity_report_controller.rb' - - 'app/controllers/login_controller.rb' - - 'app/controllers/role_distribution_report_controller.rb' - - 'app/domain/evaluations/absences_eval.rb' - - 'app/domain/evaluations/employees_eval.rb' - - 'app/domain/reports/extended_capacity_report.rb' - - 'app/models/employee.rb' - - 'test/controllers/employees/log_controller_test.rb' - - 'test/controllers/ordertimes_controller_test.rb' - - 'test/controllers/revenue_reports_controller_test.rb' - - 'test/domain/reports/role_distribution_report_test.rb' - - 'test/integration/plannings_orders_test.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -Style/ExpandPathArguments: - Exclude: - - 'Rakefile' - - 'config/application.rb' - - 'test/test_helper.rb' - # Offense count: 1 -# Configuration parameters: EnforcedStyle. -# SupportedStyles: left_coerce, right_coerce, single_coerce, fdiv -Style/FloatDivision: - Exclude: - - 'app/domain/graphs/vacation_graph.rb' - -# Offense count: 1 -# Cop supports --auto-correct. +# This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: format, sprintf, percent Style/FormatString: Exclude: - 'test/integration/invoice_form_test.rb' -# Offense count: 6 -# Configuration parameters: EnforcedStyle. -# SupportedStyles: annotated, template, unannotated -Style/FormatStringToken: - Exclude: - - 'app/helpers/graph_helper.rb' - - 'app/models/invoice.rb' - - 'app/models/user_notification.rb' - - 'app/models/util/period.rb' - - 'config/routes.rb' - - 'test/integration/invoice_form_test.rb' - -# Offense count: 493 -# Cop supports --auto-correct. +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never Style/FrozenStringLiteralComment: - Enabled: false - -# Offense count: 90 -# Configuration parameters: MinBodyLength. -Style/GuardClause: - Enabled: false - -# Offense count: 4 -# Cop supports --auto-correct. -Style/HashEachMethods: Exclude: - - 'app/domain/apidocs/setup.rb' - - 'app/domain/graphs/worktime_graph.rb' - - 'app/domain/order/cockpit/total_row.rb' - - 'db/migrate/20160929150008_create_custom_lists.rb' - -# Offense count: 6 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. -# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys -Style/HashSyntax: - Exclude: - - 'config/environments/development.rb' - - 'test/domain/plannings/item_test.rb' + - 'app/models/application_record.rb' + - 'test/models/employee_test.rb' -# Offense count: 1 -# Cop supports --auto-correct. -Style/HashTransformKeys: +# Offense count: 2 +# Configuration parameters: MinBranchesCount. +Style/HashLikeCase: Exclude: - - 'app/domain/reports/revenue/base.rb' + - 'app/domain/order/report.rb' + - 'app/helpers/order_helper.rb' -# Offense count: 100 -# Cop supports --auto-correct. +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: - Enabled: false - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: line_count_dependent, lambda, literal -Style/Lambda: Exclude: - - 'test/support/custom_assertions.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: IgnoredMethods. -Style/MethodCallWithoutArgsParentheses: - Exclude: - - 'test/domain/order/report_test.rb' - -# Offense count: 2 -Lint/MissingSuper: - Exclude: - - 'app/helpers/dry_crud/form/builder.rb' - - 'test/support/crud_test_models_controller.rb' + - 'app/controllers/order_targets_controller.rb' + - 'app/domain/forms/worktime_edit.rb' + - 'app/domain/plannings/employee_board.rb' + - 'app/models/accounting_post.rb' # Offense count: 2 Style/MissingRespondToMissing: @@ -809,288 +514,32 @@ Style/MissingRespondToMissing: # Offense count: 1 Style/MultilineBlockChain: Exclude: - - 'app/domain/invoicing/small_invoice/invoice_sync.rb' + - 'test/support/retry_on_flaky_tests.rb' # Offense count: 2 -# Cop supports --auto-correct. -Style/MultilineIfModifier: +Style/OpenStructUse: Exclude: - - 'test/helpers/dry_crud/form/builder_test.rb' - -# Offense count: 4 -# Cop supports --auto-correct. -Style/MultilineWhenThen: - Exclude: - - 'app/controllers/evaluator_controller.rb' - - 'app/helpers/format_helper.rb' - -# Offense count: 8 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: literals, strict -Style/MutableConstant: - Exclude: - - 'app/models/worktime.rb' - - 'config/version.rb' - - 'lib/tasks/license.rake' - - 'test/support/crud_test_helper.rb' - - 'test/support/crud_test_models_controller.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, MinBodyLength. -# SupportedStyles: skip_modifier_ifs, always -Style/Next: - Exclude: - - 'db/migrate/20160919101807_add_daily_plannings.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedOctalStyle. -# SupportedOctalStyles: zero_with_o, zero_only -Style/NumericLiteralPrefix: - Exclude: - - 'test/controllers/employees/log_controller_test.rb' - - 'test/helpers/worktime_helper_test.rb' - -# Offense count: 5 -# Cop supports --auto-correct. -# Configuration parameters: Strict. -Style/NumericLiterals: - MinDigits: 6 - -# Offense count: 31 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods. -# SupportedStyles: predicate, comparison -Style/NumericPredicate: - Enabled: false - -# Offense count: 2 -# Cop supports --auto-correct. -Style/ParallelAssignment: - Exclude: - - 'test/integration/invoice_form_test.rb' - - 'test/models/work_item_test.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: AllowSafeAssignment, AllowInMultilineConditions. -Style/ParenthesesAroundCondition: - Exclude: - - 'app/domain/expenses/pdf_export.rb' - -# Offense count: 78 -# Cop supports --auto-correct. -# Configuration parameters: PreferredDelimiters. -Style/PercentLiteralDelimiters: - Enabled: false - -# Offense count: 2 -# Cop supports --auto-correct. -Style/RandomWithOffset: - Exclude: - - 'test/fabricators/employment_roles_employment_fabricator.rb' - - 'test/fabricators/planning_fabricator.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -Style/RedundantBegin: - Exclude: - - 'app/domain/crm/highrise.rb' - - 'app/domain/invoicing/small_invoice/client_sync.rb' - - 'app/domain/invoicing/small_invoice/invoice_sync.rb' - -# Offense count: 4 -# Cop supports --auto-correct. -Style/RedundantCondition: - Exclude: - - 'app/domain/evaluations/sub_work_items_eval.rb' - - 'app/domain/graphs/vacation_graph.rb' - - 'app/domain/order/controlling.rb' - - 'app/models/employment.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Style/RedundantParentheses: - Exclude: - - 'test/support/crud_controller_test_helper.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: AllowMultipleReturnValues. -Style/RedundantReturn: - Exclude: - - 'app/controllers/worktimes_controller.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Style/RedundantSelf: - Exclude: - - 'app/models/employee.rb' - - 'config/application.rb' - -# Offense count: 18 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, AllowInnerSlashes. -# SupportedStyles: slashes, percent_r, mixed -Style/RegexpLiteral: - Exclude: - - 'app/helpers/navigation_helper.rb' - - 'test/helpers/dry_crud/form/builder_test.rb' - - 'test/helpers/form_helper_test.rb' - - 'test/helpers/table_helper_test.rb' - - 'test/support/crud_test_helper.rb' - -# Offense count: 6 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: implicit, explicit -Style/RescueStandardError: - Exclude: - - 'app/domain/crm/highrise.rb' - - 'app/domain/forms/multi_absence.rb' - - 'app/domain/invoicing/small_invoice/client_sync.rb' - - 'app/domain/invoicing/small_invoice/invoice_sync.rb' - - 'app/helpers/format_helper.rb' - - 'app/models/util/period.rb' + - 'app/controllers/order_services_controller.rb' + - 'test/controllers/employees_controller_test.rb' # Offense count: 23 -# Cop supports --auto-correct. -# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods. -# AllowedMethods: present?, blank?, presence, try, try! -Style/SafeNavigation: +# Configuration parameters: AllowedMethods. +# AllowedMethods: respond_to_missing? +Style/OptionalBooleanParameter: Enabled: false # Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: AllowAsExpressionSeparator. -Style/Semicolon: - Exclude: - - 'test/models/invoice_test.rb' - -# Offense count: 12 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: only_raise, only_fail, semantic -Style/SignalException: - Exclude: - - 'app/controllers/application_controller.rb' - - 'app/controllers/concerns/with_period.rb' - - 'app/controllers/invoices_controller.rb' - - 'app/controllers/periods_controller.rb' - - 'app/domain/crm/highrise.rb' - - 'app/domain/invoicing/small_invoice/api.rb' - - 'app/domain/plannings/creator.rb' - - 'config/initializers/session_store.rb' - - 'test/support/crud_controller_test_helper.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: AllowIfMethodIsEmpty. -Style/SingleLineMethods: - Exclude: - - 'app/controllers/expenses_reviews_controller.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Style/StderrPuts: - Exclude: - - 'lib/tasks/db.rake' - -# Offense count: 63 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. -# SupportedStyles: single_quotes, double_quotes -Style/StringLiterals: - Exclude: - - 'app/helpers/accounting_posts_helper.rb' - - 'config/environments/development.rb' - - 'config/initializers/better_errors.rb' - - 'config/initializers/session_store.rb' - - 'config/puma.rb' - - 'db/migrate/20170321121326_create_versions.rb' - - 'db/migrate/20190204150027_create_active_storage_tables.active_storage.rb' - - 'db/migrate/20190214100801_remove_classic_delayed_jobs.rb' - - 'test/controllers/evaluator_controller_test.rb' - - 'test/controllers/expenses_controller_test.rb' - - 'test/controllers/expenses_reviews_controller_test.rb' - - 'test/domain/reports/workload_test.rb' - - 'test/integration/invoice_form_test.rb' - - 'test/integration/order_reports_test.rb' - - 'test/models/employment_test.rb' - -# Offense count: 5 -Style/StructInheritance: - Exclude: - - 'app/domain/order/cockpit/cell.rb' - - 'app/domain/order/cockpit/row.rb' - - 'app/domain/reports/workload/ordertime_entry.rb' - - 'app/domain/reports/workload/worktime_entry.rb' - - 'app/helpers/dry_crud/table/col.rb' - -# Offense count: 114 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, MinSize. -# SupportedStyles: percent, brackets -Style/SymbolArray: - Enabled: false - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: IgnoredMethods. -# IgnoredMethods: respond_to, define_method -Style/SymbolProc: - Exclude: - - 'app/domain/order/cockpit/accounting_post_row.rb' - - 'test/domain/plannings/order_board_test.rb' - -# Offense count: 9 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma -Style/TrailingCommaInArguments: - Exclude: - - 'db/seeds/development/absences.rb' - - 'db/seeds/development/departments.rb' - - 'db/seeds/development/employees.rb' - - 'db/seeds/development/order_statuses.rb' - - 'db/seeds/development/zz_work_item.rb' - - 'test/models/expense_test.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma -Style/TrailingCommaInArrayLiteral: - Exclude: - - 'test/domain/plannings/creator_test.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Style/WhileUntilModifier: +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Mode. +Style/StringConcatenation: Exclude: + - 'app/controllers/evaluator_controller.rb' + - 'app/domain/order/services/csv_filename_generator.rb' - 'lib/tasks/license.rake' -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: WordRegex. -# SupportedStyles: percent, brackets -Style/WordArray: - EnforcedStyle: percent - MinSize: 3 - -# Offense count: 2 -# Cop supports --auto-correct. -Style/ZeroLengthPredicate: - Exclude: - - 'db/migrate/20160919101807_add_daily_plannings.rb' - - 'test/models/order_test.rb' - -# Offense count: 1561 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# Offense count: 26 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. # URISchemes: http, https Layout/LineLength: - Max: 236 -... + Max: 234 diff --git a/.ruby-version b/.ruby-version index 37c2961c2..e4604e3af 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.2 +3.2.1 diff --git a/.s2i/environment b/.s2i/environment deleted file mode 100644 index fc5356ede..000000000 --- a/.s2i/environment +++ /dev/null @@ -1 +0,0 @@ -S2I_BUILD=true \ No newline at end of file diff --git a/.tool-versions b/.tool-versions index 9eb38ed71..40b87ee46 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -ruby 2.7.2 +ruby 3.2.1 diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 6300bbbd3..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "rubyTestExplorer.logpanel": true, - "rubyTestExplorer.testFramework": "minitest", - "saveEditorLayout.list": [ - { - "name": "ptime: api tests", - "documents": [ - { - "fsPath": "/code/pz/puzzletime/test/small_invoice_test_helper.rb", - "column": 1 - }, - { - "fsPath": "/code/pz/puzzletime/app/domain/invoicing/small_invoice/contact_sync.rb", - "column": 1 - }, - { - "fsPath": "/code/pz/puzzletime/test/domain/invoicing/small_invoice/contact_sync_test.rb", - "column": 1 - }, - { - "fsPath": "/code/pz/puzzletime/test/domain/invoicing/small_invoice/api_test.rb", - "column": 2 - }, - { - "fsPath": "/code/pz/puzzletime/test/domain/invoicing/small_invoice/address_sync_test.rb", - "column": 2 - }, - { - "fsPath": "/code/pz/puzzletime/test/domain/invoicing/small_invoice/interface_test.rb", - "column": 2 - }, - { - "fsPath": "/code/pz/puzzletime/test/domain/invoicing/small_invoice/invoice_store_test.rb", - "column": 2 - } - ] - } - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b936c036e..8e9f5310b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,50 @@ -# master branch unreleased +# 2.11 + +### Improvements + +* **Update:** Ruby auf Version 3.2.1 aktualisiert +* **Update:** Rails auf Version 7.1.3 aktualisiert +* **Update:** Alle Dependencies aktualisiert +* **Build Pipeline:** Github Actions werden nun verwendet + +# 2.10 + +### Improvements + +* **Login:** Auto-redirect zum SSO login sofern genau 1 SSO Provider konfiguriert ist und localauth deaktiviert ist +* **Sicherheit:** Secure flag auf session cookie gesetzt +* **UX:** Absenztyp Filter wird nun auch für Absenzen Export respektiert + +# 2.9 + +### Improvements + +* **Log:** Änderungen an den Funktionsanteilen der Anstellungen werden neu im Members-Log protokolliert +* **Absenzen:** In der Auswertung kann nach Absenztyp gefiltert werden +* **Auslastung:** Verwendet nun die Standard Zeitbereich Auswahl. +* **CSV Detaillierte Auslastung:** + + Berücksichtigt nun den eingestellten Zeitbereich + + Berechnung des durchschnittlichen Arbeitspensums korrigiert + + Spalte hinzugefügt für "bereinigte Projektzeit" + +# 2.8 + ### Features -* **Login:** Unterstützt nun Omniauth mit Keycloak und/oder SAML -* **Rechnungsstellung:** Umstellung auf SmallInvoice APIv2 (vorher v1) + +* **Login:** Login wird auf SSO (Keycloak, Devise) umgestellt +* **Zeitfreigabe:** Neu wird eine Erinnerung per E-Mail versendet, wenn die Zeiten noch nicht freigegeben wurden. + + ### Improvements +* **Stammdaten:** In den Stammdaten der Members wird neu der vertragliche Arbeitsort geführt +* **Log:** Die Änderungen der Anstellungen (Pensen, Funktionen) wird neu im Members-Log protokolliert # 2.7 ### Features -* **Business Intelligence:** Wir können jetzt Verbindung zu einer InfluxDB herstellen, die wichtige Kennzahlen als Timeseries speichert +* **Login:** Unterstützt nun Omniauth mit Keycloak und/oder SAML +* **Business Intelligence:** Wir können jetzt Verbindung zu einer InfluxDB herstellen, die wichtige Kennzahlen als Timeseries speichert + ### Improvements * **Update:** Update auf Ruby 2.7 * **Exporte:** Die verschiedenen CSV Exporte in einen Controller refactored @@ -17,8 +54,10 @@ * **Dokumentation:** Das Herokusetup ist jetzt dokumentiert * **Spesen:** Spesenbelege werden nun beim Hochladen herunterskaliert * **Kundenauswertung:** Auftrag verlinkt, um schneller hin und her navigieren zu können -* **Mitarbeiter-Stammdaten:** Attribut "Telefon privat" umbenannt in "Mobiltelefon" -* **Mitarbeiter-Stammdaten:** Anstellungsprozente und Funktionsanteile können nun in 2.5% Schritten konfiguriert werden +* **Mitarbeiter-Stammdaten:** + + Attribut "Telefon privat" umbenannt in "Mobiltelefon" + + Anstellungsprozente und Funktionsanteile können nun in 2.5% Schritten konfiguriert werden + + Neues Attribut "Arbeitsort", verfügbare Werte konfigurierbar unter "Verwalten" * **Mitarbeiterliste:** Sortierbar gemacht nach Vorname, Nachname * **Zeiterfassung:** Leerschläge vor und nach der Ticketnummer werden entfernt @@ -27,13 +66,16 @@ * **Verbleibende Arbeitszeit:** Berechnung korrigiert wenn Überstundenkompensationen in der Zukunft liegen # 2.6 + ### Features + * **Verpflegungsentschädigung:** Bei der Arbeitszeiterfassung kann zusätzlich angegeben werden, ob die Arbeit beim Kunden vor Ort erfolgte und dazu eine Verpflegungsentschädigung gewünscht wird. * **Mitarbeiter-Stammdaten:** Ausweisinformationen können nun hinzugefügt werden. * **Buchungspositionen:** Einstellungen zu Ticket, Von-Bis-Zeiten und Bemerkungen können nicht mehr geändert werden, falls bereits Leistungen ohne diese Angaben erfasst wurden. * **Buchungspositionen:** Auftrags-Cockpit mit neuen Informationen ergänzt. ### Improvements + * **Usability:** Unter "Members" - "Zeiten" wird die Tabelle standardmässig nach Members der eigenen Organisationseinheit gefiltert, was die Bedienung und Ladegeschwindigkeit massiv erhöht. * **Usability:** Im Zeiterfassungs-Formular können nun auch alte Zeiteinträge dupliziert werden. * **Usability:** Auftragsverantwortliche dürfen die AHV-Nummern aller Members einsehen. @@ -41,51 +83,63 @@ * **Sicherheit:** Updates diverser rubygems aus Sicherheitsgründen. ### Bug fixes + * **Stundenübersicht:** Falsches Total berichtigt. * **Buchungspositionen:** Automatische Budget-Berechnung beim Eintragen korrigiert. * **Mitarbeiterliste:** Falsche Berechnung des Jubiläum (Dienstjahre) [\#61](https://github.com/puzzle/puzzletime/issues/61) - # 2.5 ### Improvements + * **Layout:** Die Navigationsleiste ist nun sticky [\#29](https://github.com/puzzle/puzzletime/issues/29) + * **Wording:** Mitarbeiter heissen neu Members. + * **Absenzen:** Mit Management-Berechtigung können nun Absenzen der anderen Members gelöscht werden. + * **Zeitfreigabe:** Die Zeitfreigabe wird neu im Log des Members angezeigt. + * **Rechnungen:** Manuelle Rechnung, die im Rechnungsstellungtool editiert wurden, können in PuzzleTime nicht mehr versehentlich überschrieben werden. + * **Mitarbeiterblatt:** Die AHV-Nummer der Members wird nur noch mit Management-Berechtigung angezeigt [\#23](https://github.com/puzzle/puzzletime/issues/23) * **Umsatzberechnung:** Fälschlicherweise verrechenbar gebuchte Stunden auf Puzzle werden nun nicht mehr mit einbezogen. + * **Umsatz:** Gibt es jetzt als CSV Export. + * **Feiertage:** Neu können alle Feiertage frei konfiguriert werden. + * **Sicherheit:** Updates diverser rubygems aus Sicherheitsgründen. ### Bug fixes + * **Login:** Bei fehlerhaftem Login wird die Meldung nun in der Warnfarbe dargestellt. * **Wochenübersicht Stunden:** Sollstundenlinie verschiebt sich nicht mehr. * **Zeitbuchung:** Es kann nun nur noch von 00:00-23:59 gebucht werden um Fehlern vorzubeugen. * **Budget-Controlling:** Submenü wird nun wieder korrekt dargestellt. * **Browsersupport:** Projektsuche funktioniert wieder auf IE11. - # 2.4 ### Features + * **Spesen:** Neu können in PuzzleTime Spesen hochgeladen und freigegeben resp. abgelehnt werden. * **API:** Ein neues json:api mit Lesezugriff, vorerst nur für /employees. Unter `/api/docs` ist ein Swagger UI mit der Dokumentation verfügbar. ### Improvements + * **Umsatz:** Auftragsverantwortliche haben nun auch Zugriff auf den Umsatz. ### Bug Fixes + * **Zeiterfassung:** Usability Fehler beim Duplizieren von Zeiteinträgen geflickt [\#28](https://github.com/puzzle/puzzletime/issues/28) * **Zeiterfassung:** Beim Zeiterfassen mit Firefox kann mit Tab wieder von der Buchungsposition weitergesprungen werden [\#34](https://github.com/puzzle/puzzletime/issues/34) - # 2.3 ### Improvements + * **Ruby/Rails:** Auf Ruby 2.5.3 und Rails 5.2.2 aktualisiert * **Mitarbeiter-Stammdaten:** Neu können bei den Mitarbeitern Nationalitäten und der (Hochschul-)Abschluss erfasst werden. * **Rechnungen:** Unter Aufträge - In einem einzelnen Auftrag - Rechnungen wurden die Summen verbessert um einen besseren Überblick über bezahlte und offene Stunden zu erhalten. @@ -94,11 +148,13 @@ * **Konfigurierbarkeit:** ID der betreibenden Firma, MwST, Währung und Land können nun konfiguriert werden. ### Bug Fixes + * **Wirtschaftlichkeit:** Unter Aufträge - In einem einzelnen Auftrag - Positionen werden in der Berechnung der Wirtschaftlichkeit die stornierten Rechnungen nicht mehr mit einberechnet. # 2.2 ### Features + * **Budget-Controlling I:** Unter Aufträge - Controlling sieht man anhand eines Fortschrittsbalken, wie viele Stunden vom Gesamtbudget schon geleistet wurden. Ein Klick darauf führt ins neue Budget-Controlling Tab des entsprechenden Auftrages. * **Budget-Controllig II:** Im Budget-Controlling Tab eines Auftrages sieht man anhand eines chicen Balkendiagramms, wann wie viele Stunden geleistet wurden und wie viele Stunden in der Zukunft provisorisch und definitiv geplant sind. * **Zeitkontrolle:** Zeitfreigabe und -kontrolle ist nun auch für die Auftragsverantwortlichen (unter Aufträge - Meine Aufträge) ersichtlich @@ -106,6 +162,7 @@ * **Fremde Arbeitszeiten löschen:** Mit Management-Berechtigung können die Arbeitszeiten anderen Mitarbeiter gelöscht werden. Diese werden per E-Mail darüber informiert, wer wann welchen Eintrag gelöscht hat. ### Bug Fixes + * **Planung:** Planungseinträge gehen nicht mehr verloren, wenn in einem Auftrag ohne Buchungspositionen nachträglich Buchungspositionen erstellt werden * **Planungswiederholung:** Eine Planungswiederholung kann nun auch bis am 31.12.2018 erstellt werden, denn dieses Datum trifft ausnahmsweise auf die Kalenderwoche 1 des Folgejahres 2019. * **Mitarbeiterblatt:** Das Mitarbeiterblatt (unter Mitarbeiter - Zeiten - Mitarbeiter auswählen) sieht nun auch gedruckt gut aus und passt auf eine Seite (querformat). @@ -120,7 +177,6 @@ * **Zeitkontrolle:** Die Zeitkontrolle kann nun im PuzzleTime unter "Auswertungen" - "Mitarbeit" - "Kontrolle" gemacht werden * **Jubiläum:** In der Mitarbeiterliste werden nun die Anzahl Dienstjahre der Mitarbeiter angezeigt - ### Bug Fixes * **Volltextsuche:** Volltextsuche der Buchungspositionen geflickt @@ -128,11 +184,8 @@ * **MWST:** PuzzleTime kann nun mit mehreren MWST-Sätzen korrekt rechnen * **Absenzen:** Die Sichtbarkeit der Absenzen bereinigen - ### Improvements * **Ruby/Rails:** Auf Ruby 2.2.2 und Rails 5.1.2 aktualisiert * **Performance:** Chrome Memory Leak in Plannings behoben * **Usability:** Menüstruktur reorganisiert - - diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..79a0c6cdc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,207 @@ +################################# +# Variables # +################################# + +# Versioning +ARG RUBY_VERSION="3.2.1" +ARG BUNDLER_VERSION="2.4.6" + +# Packages +# ARG BUILD_PACKAGES="nodejs build-essential libc6" +ARG BUILD_PACKAGES +ARG RUN_PACKAGES="bash libpq5 libvips42 libvips-dev" + +# Scripts +ARG PRE_INSTALL_SCRIPT +ARG INSTALL_SCRIPT +ARG PRE_BUILD_SCRIPT +ARG BUILD_SCRIPT="bundle exec rake assets:precompile" +ARG POST_BUILD_SCRIPT="echo \"(built at: $(date '+%Y-%m-%d %H:%M:%S'))\" > /app-src/BUILD_INFO" + +# Bundler specific +ARG BUNDLE_WITHOUT="development:metrics:test" + +# App specific +ARG RAILS_ENV="production" +ARG RACK_ENV="production" +ARG RAILS_HOST_NAME="unused.example.net" +ARG SECRET_KEY_BASE="needs-to-be-set" +ARG RAILS_DB_ADAPTER="nulldb" + +# Github specific +ARG GITHUB_SHA +ARG GITHUB_REPOSITORY +ARG GITHUB_REF_NAME +ARG BUILD_COMMIT="$GITHUB_SHA" +ARG BUILD_REPO="$GITHUB_REPOSITORY" +ARG BUILD_REF="$GITHUB_REF_NAME" + +# # Gitlab specific +# ARG CI_COMMIT_SHA +# ARG CI_REPOSITORY_URL +# ARG CI_COMMIT_REF_NAME +# ARG BUILD_COMMIT="$CI_COMMIT_SHA" +# ARG BUILD_REPO="$CI_REPOSITORY_URL" +# ARG BUILD_REF="$CI_COMMIT_REF_NAME" + +# # Openshift specific +# ARG OPENSHIFT_BUILD_COMMIT +# ARG OPENSHIFT_BUILD_SOURCE +# ARG OPENSHIFT_BUILD_REFERENCE +# ARG BUILD_COMMIT="$OPENSHIFT_BUILD_COMMIT" +# ARG BUILD_REPO="$OPENSHIFT_BUILD_SOURCE" +# ARG BUILD_REF="$OPENSHIFT_BUILD_REFERENCE" + +# Runtime ENV vars +ARG SENTRY_CURRENT_ENV +ARG PS1="$SENTRY_CURRENT_ENV > " +ARG TZ="Europe/Zurich" + +# Custom ARGs for Puzzletime +ARG SKIP_MEMCACHE_CHECK=true + +################################# +# Build Stage # +################################# + +FROM ruby:${RUBY_VERSION} AS build + +# arguments for steps +ARG PRE_INSTALL_SCRIPT +ARG BUILD_PACKAGES +ARG INSTALL_SCRIPT +ARG BUNDLER_VERSION +ARG PRE_BUILD_SCRIPT +ARG BUNDLE_WITHOUT +ARG BUILD_SCRIPT +ARG POST_BUILD_SCRIPT + +# arguments potentially used by steps +ARG RACK_ENV +ARG RAILS_ENV +ARG RAILS_HOST_NAME +ARG SECRET_KEY_BASE +ARG TZ + +# Custom ARGs +ARG SKIP_MEMCACHE_CHECK + +# Set build shell +SHELL ["/bin/bash", "-c"] + +# Use root user +USER root + +RUN bash -vxc "${PRE_INSTALL_SCRIPT:-"echo 'no PRE_INSTALL_SCRIPT provided'"}" + +# Install dependencies +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y --no-install-recommends ${BUILD_PACKAGES} + +RUN bash -vxc "${INSTALL_SCRIPT:-"echo 'no INSTALL_SCRIPT provided'"}" + +# Install specific versions of dependencies +RUN gem install bundler:${BUNDLER_VERSION} --no-document + +# Install nulldb adapter +ARG RAILS_DB_ADAPTER +RUN gem install activerecord-nulldb-adapter --no-document + +# TODO: Load artifacts + +# set up app-src directory +COPY . /app-src +WORKDIR /app-src + +RUN bash -vxc "${PRE_BUILD_SCRIPT:-"echo 'no PRE_BUILD_SCRIPT provided'"}" + +# install gems and build the app +RUN bundle config set --local deployment 'true' \ + && bundle config set --local without ${BUNDLE_WITHOUT} \ + && bundle package \ + && bundle install \ + && bundle clean + +RUN bash -vxc "${BUILD_SCRIPT:-"echo 'no BUILD_SCRIPT provided'"}" + +RUN bash -vxc "${POST_BUILD_SCRIPT:-"echo 'no POST_BUILD_SCRIPT provided'"}" + +# TODO: Save artifacts + +RUN rm -rf vendor/cache/ .git spec/ node_modules/ + + +################################# +# Run Stage # +################################# + +# This image will be replaced by Openshift +FROM ruby:${RUBY_VERSION}-slim AS app + +# Set runtime shell +SHELL ["/bin/bash", "-c"] + +# Add user +RUN adduser --disabled-password --uid 1001 --gid 0 --gecos "" app + +# arguments for steps +ARG RUN_PACKAGES +ARG BUNDLER_VERSION +ARG BUNDLE_WITHOUT + +# arguments potentially used by steps +ARG RACK_ENV +ARG RAILS_ENV + +# data persisted in the image +ARG PS1 +ARG TZ +ARG BUILD_COMMIT +ARG BUILD_REPO +ARG BUILD_REF + +# Custom ARGs +ARG SKIP_MEMCACHE_CHECK + +ENV PS1="${PS1}" \ + TZ="${TZ}" \ + BUILD_REPO="${BUILD_REPO}" \ + BUILD_REF="${BUILD_REF}" \ + BUILD_COMMIT="${BUILD_COMMIT}" \ + RAILS_ENV="${RAILS_ENV}" \ + RACK_ENV="${RACK_ENV}" + +# Install dependencies, remove apt! +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y ${RUN_PACKAGES} vim curl less \ + && apt-get clean \ + && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* /tmp/* /var/tmp/* \ + && truncate -s 0 /var/log/*log + +# Copy deployment ready source code from build +COPY --from=build /app-src /app-src +WORKDIR /app-src + +# Set group permissions to app folder +RUN chgrp -R 0 /app-src \ + && chmod -R u+w,g=u /app-src + +# support bin-stubs +ENV HOME=/app-src \ + PATH=/app-src/bin:$PATH + +# Install specific versions of dependencies +RUN gem install bundler:${BUNDLER_VERSION} --no-document + +# Use cached gems +RUN bundle config set --local deployment 'true' \ + && bundle config set --local without ${BUNDLE_WITHOUT} \ + && bundle install + +USER 1001 + +CMD ["bundle", "exec", "puma", "-t", "8"] diff --git a/Gemfile b/Gemfile index 79197e83e..9baa89eb1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,15 +1,22 @@ +# frozen_string_literal: true + source 'https://rubygems.org' -gem 'rails', '~> 5.2.x' +git_source(:github) { |name| "https://github.com/#{name}.git" } + +gem 'rails', '~> 7.0.x' +gem 'activerecord-nulldb-adapter', github: 'puzzle/nulldb' gem 'pg' gem 'nochmal', github: 'puzzle/nochmal' gem 'acts_as_tree' +gem 'annotate' gem 'aws-sdk-s3', require: false gem 'bleib' gem 'bootsnap' +gem 'brakeman' gem 'cancancan' gem 'config' gem 'country_select' @@ -18,15 +25,19 @@ gem 'dalli' gem 'delayed_cron_job' gem 'delayed_job_active_record' gem 'devise' +gem 'email_address' gem 'fast_jsonapi' gem 'haml' +gem 'haml-lint' gem 'highrise' gem 'image_processing' -gem 'influxdb-client' -gem 'influxdb-client-apis' gem 'jbuilder' gem 'kaminari' gem 'kaminari-bootstrap' +gem 'listen' +gem 'matrix' +gem 'mini_racer' +gem 'minitest-reporters' gem 'nested_form_fields' gem 'net-ldap' gem 'nokogiri' @@ -37,12 +48,20 @@ gem 'omniauth-saml' gem 'prawn' gem 'prometheus_exporter' gem 'protective' +gem 'psych' gem 'puma' gem 'rails_autolink' +gem 'rails-erd' gem 'rails-i18n' gem 'request_store' gem 'rqrcode' gem 'rswag-ui' +gem 'rubocop' +gem 'rubocop-checkstyle_formatter', require: false +gem 'rubocop-minitest' +gem 'rubocop-performance' +gem 'rubocop-rails' +gem 'sdoc' gem 'seed-fu' gem 'swagger-blocks' gem 'validates_by_schema' @@ -62,8 +81,13 @@ gem 'jquery-rails' gem 'jquery-ui-rails' gem 'sass-rails' gem 'selectize-rails' +gem 'terser' gem 'turbolinks' -gem 'uglifier' + +# debugging +gem 'pry-byebug', require: ENV['RM_INFO'].to_s.empty? +gem 'pry-doc' +gem 'pry-rails' # must be at the end gem 'paper_trail' @@ -73,7 +97,6 @@ group :development, :test do gem 'binding_of_caller' gem 'codez-tarantula', require: 'tarantula-rails3' gem 'faker' - gem 'pry-rails' gem 'rb-readline' gem 'request_profiler' end @@ -86,33 +109,12 @@ end group :test do gem 'bundler-audit' - gem 'capybara' + gem 'cuprite' gem 'database_cleaner' gem 'fabrication' - gem 'headless' gem 'mocha', require: false gem 'rails-controller-testing' + gem 'webmock' gem 'selenium-webdriver' gem 'webdrivers' - gem 'webmock' -end - -group :console do - gem 'pry-byebug', require: ENV['RM_INFO'].to_s.empty? - gem 'pry-doc' -end - -group :metrics do - gem 'annotate' - gem 'brakeman' - gem 'haml-lint' - gem 'minitest-reporters' - gem 'rails-erd' - gem 'rubocop' - gem 'rubocop-checkstyle_formatter', require: false - gem 'rubocop-minitest' - gem 'rubocop-performance' - gem 'rubocop-rails' - gem 'sdoc' - gem 'simplecov-rcov', git: 'https://github.com/puzzle/simplecov-rcov' end diff --git a/Gemfile.lock b/Gemfile.lock index a1f818a24..db988d9c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,137 +1,174 @@ GIT remote: https://github.com/puzzle/nochmal.git - revision: d66a5e9a99e550084d29763d1395e1529fa17c2a + revision: 432dc68af1dabf8943f40366e39465eab117eec8 specs: - nochmal (0.1.0) + nochmal (0.2.1) + pastel + rails (>= 6.0) GIT - remote: https://github.com/puzzle/simplecov-rcov - revision: 554ec457f4a7bdceae083ce516c281567a341e4a + remote: https://github.com/puzzle/nulldb.git + revision: 06088c94b496f3ecd6017c759bc0b2353b50e89a specs: - simplecov-rcov (0.2.3.1) - simplecov (>= 0.4.1) + activerecord-nulldb-adapter (1.0.1) + activerecord (>= 5.2.0, < 7.2) GEM remote: https://rubygems.org/ specs: - actioncable (5.2.6) - actionpack (= 5.2.6) + actioncable (7.1.3) + actionpack (= 7.1.3) + activesupport (= 7.1.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.6) - actionpack (= 5.2.6) - actionview (= 5.2.6) - activejob (= 5.2.6) + zeitwerk (~> 2.6) + actionmailbox (7.1.3) + actionpack (= 7.1.3) + activejob (= 7.1.3) + activerecord (= 7.1.3) + activestorage (= 7.1.3) + activesupport (= 7.1.3) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.1.3) + actionpack (= 7.1.3) + actionview (= 7.1.3) + activejob (= 7.1.3) + activesupport (= 7.1.3) mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (5.2.6) - actionview (= 5.2.6) - activesupport (= 5.2.6) - rack (~> 2.0, >= 2.0.8) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.2) + actionpack (7.1.3) + actionview (= 7.1.3) + activesupport (= 7.1.3) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.6) - activesupport (= 5.2.6) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.3) + actionpack (= 7.1.3) + activerecord (= 7.1.3) + activestorage (= 7.1.3) + activesupport (= 7.1.3) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.1.3) + activesupport (= 7.1.3) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.6) - activesupport (= 5.2.6) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.3) + activesupport (= 7.1.3) globalid (>= 0.3.6) - activemodel (5.2.6) - activesupport (= 5.2.6) + activemodel (7.1.3) + activesupport (= 7.1.3) activemodel-serializers-xml (1.0.2) activemodel (> 5.x) activesupport (> 5.x) builder (~> 3.1) - activerecord (5.2.6) - activemodel (= 5.2.6) - activesupport (= 5.2.6) - arel (>= 9.0) - activeresource (5.1.1) - activemodel (>= 5.0, < 7) + activerecord (7.1.3) + activemodel (= 7.1.3) + activesupport (= 7.1.3) + timeout (>= 0.4.0) + activeresource (6.0.0) + activemodel (>= 6.0) activemodel-serializers-xml (~> 1.0) - activesupport (>= 5.0, < 7) - activestorage (5.2.6) - actionpack (= 5.2.6) - activerecord (= 5.2.6) - marcel (~> 1.0.0) - activesupport (5.2.6) + activesupport (>= 6.0) + activestorage (7.1.3) + actionpack (= 7.1.3) + activejob (= 7.1.3) + activerecord (= 7.1.3) + activesupport (= 7.1.3) + marcel (~> 1.0) + activesupport (7.1.3) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) acts_as_tree (2.9.1) activerecord (>= 3.0.0) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) - airbrake (11.0.3) - airbrake-ruby (~> 5.1) - airbrake-ruby (5.2.1) - rbtree3 (~> 0.5) - annotate (3.1.1) - activerecord (>= 3.2, < 7.0) + airbrake (13.0.4) + airbrake-ruby (~> 6.0) + airbrake-ruby (6.2.2) + rbtree3 (~> 0.6) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ansi (1.5.0) - arel (9.0.0) ast (2.4.2) - autoprefixer-rails (10.2.5.1) - execjs (> 0) - aws-eventstream (1.2.0) - aws-partitions (1.496.0) - aws-sdk-core (3.121.0) - aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.239.0) - aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-kms (1.48.0) - aws-sdk-core (~> 3, >= 3.120.0) + autoprefixer-rails (10.4.16.0) + execjs (~> 2) + aws-eventstream (1.3.0) + aws-partitions (1.883.0) + aws-sdk-core (3.190.3) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.8) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.76.0) + aws-sdk-core (~> 3, >= 3.188.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.102.0) - aws-sdk-core (~> 3, >= 3.120.0) + aws-sdk-s3 (1.142.0) + aws-sdk-core (~> 3, >= 3.189.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) - aws-sigv4 (1.4.0) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) - bcrypt (3.1.16) - better_errors (2.9.1) - coderay (>= 1.0.0) + base64 (0.2.0) + bcrypt (3.1.20) + better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) - bindata (2.4.10) + rouge (>= 1.0.0) + bigdecimal (3.1.6) + bindata (2.4.15) bindex (0.8.1) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - bleib (0.0.11) - bootsnap (1.7.5) - msgpack (~> 1.0) + bleib (0.0.13) + bootsnap (1.17.1) + msgpack (~> 1.2) bootstrap-sass (3.4.1) autoprefixer-rails (>= 5.2.1) sassc (>= 2.0.0) - brakeman (5.0.4) + brakeman (6.1.1) + racc builder (3.2.4) - bullet (6.1.4) + bullet (7.1.6) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) - bundler-audit (0.8.0) + bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) byebug (11.1.3) - cancancan (3.3.0) - capybara (3.35.3) + cancancan (3.5.0) + capybara (3.39.2) addressable + matrix mini_mime (>= 0.1.3) nokogiri (~> 1.8) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - chartjs-ror (3.6.4) + chartjs-ror (3.7.0) rails (>= 3.1) - childprocess (3.0.0) choice (0.2.0) chunky_png (1.4.0) coderay (1.1.3) @@ -145,343 +182,406 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.1.9) - config (3.1.0) + concurrent-ruby (1.2.3) + config (5.1.0) deep_merge (~> 1.2, >= 1.2.1) dry-validation (~> 1.0, >= 1.0.0) - countries (4.0.0) - i18n_data (~> 0.13.0) - sixarm_ruby_unaccent (~> 1.1) - country_select (6.0.0) - countries (~> 4.0) - sort_alphabetical (~> 1.1) + connection_pool (2.4.1) + countries (5.7.1) + unaccent (~> 0.3) + country_select (8.0.3) + countries (~> 5.0) crack (0.4.5) rexml crass (1.0.6) - daemons (1.4.0) - dalli (2.7.11) - database_cleaner (2.0.1) - database_cleaner-active_record (~> 2.0.0) - database_cleaner-active_record (2.0.1) + cuprite (0.15) + capybara (~> 3.0) + ferrum (~> 0.14.0) + daemons (1.4.1) + dalli (3.2.6) + database_cleaner (2.0.2) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.1.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - debug_inspector (1.1.0) - deep_merge (1.2.1) - delayed_cron_job (0.7.4) - delayed_job (>= 4.1) - delayed_job (4.1.9) - activesupport (>= 3.0, < 6.2) - delayed_job_active_record (4.1.6) - activerecord (>= 3.0, < 6.2) + date (3.3.4) + debug_inspector (1.2.0) + deep_merge (1.2.2) + delayed_cron_job (0.9.0) + fugit (>= 1.5) + delayed_job (4.1.11) + activesupport (>= 3.0, < 8.0) + delayed_job_active_record (4.1.8) + activerecord (>= 3.0, < 8.0) delayed_job (>= 3.0, < 5) - devise (4.8.0) + devise (4.9.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - docile (1.4.0) - dry-configurable (0.12.1) + drb (2.2.0) + ruby2_keywords + dry-configurable (1.1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-core (1.0.1) concurrent-ruby (~> 1.0) - dry-core (~> 0.5, >= 0.5.0) - dry-container (0.7.2) + zeitwerk (~> 2.6) + dry-inflector (1.0.0) + dry-initializer (3.1.1) + dry-logic (1.5.0) concurrent-ruby (~> 1.0) - dry-configurable (~> 0.1, >= 0.1.3) - dry-core (0.6.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-schema (1.13.3) concurrent-ruby (~> 1.0) - dry-equalizer (0.3.0) - dry-inflector (0.2.0) - dry-initializer (3.0.4) - dry-logic (1.2.0) - concurrent-ruby (~> 1.0) - dry-core (~> 0.5, >= 0.5) - dry-schema (1.6.2) - concurrent-ruby (~> 1.0) - dry-configurable (~> 0.8, >= 0.8.3) - dry-core (~> 0.5, >= 0.5) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.0, < 2) dry-initializer (~> 3.0) - dry-logic (~> 1.0) - dry-types (~> 1.5) - dry-types (1.5.1) + dry-logic (>= 1.4, < 2) + dry-types (>= 1.7, < 2) + zeitwerk (~> 2.6) + dry-types (1.7.2) + bigdecimal (~> 3.0) concurrent-ruby (~> 1.0) - dry-container (~> 0.3) - dry-core (~> 0.5, >= 0.5) - dry-inflector (~> 0.1, >= 0.1.2) - dry-logic (~> 1.0, >= 1.0.2) - dry-validation (1.6.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + dry-validation (1.10.0) concurrent-ruby (~> 1.0) - dry-container (~> 0.7, >= 0.7.1) - dry-core (~> 0.4) - dry-equalizer (~> 0.2) + dry-core (~> 1.0, < 2) dry-initializer (~> 3.0) - dry-schema (~> 1.5, >= 1.5.2) - erubi (1.10.0) - ethon (0.14.0) - ffi (>= 1.15.0) - execjs (2.8.1) - fabrication (2.22.0) - faker (2.18.0) - i18n (>= 1.6, < 2) - faraday (1.5.1) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0.1) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.1) - faraday-patron (~> 1.0) - multipart-post (>= 1.2, < 3) - ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-net_http (1.0.1) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) + dry-schema (>= 1.12, < 2) + zeitwerk (~> 2.6) + email_address (0.2.4) + simpleidn + erubi (1.12.0) + et-orbi (1.2.7) + tzinfo + execjs (2.9.1) + fabrication (2.31.0) + faker (3.2.3) + i18n (>= 1.8.11, < 2) + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) + faraday-follow_redirects (0.3.0) + faraday (>= 1, < 3) + faraday-net_http (3.1.0) + net-http fast_jsonapi (1.5) activesupport (>= 4.2) - ffi (1.15.3) - globalid (0.4.2) - activesupport (>= 4.2.0) - haml (5.2.1) - temple (>= 0.8.0) + ferrum (0.14) + addressable (~> 2.5) + concurrent-ruby (~> 1.1) + webrick (~> 1.7) + websocket-driver (>= 0.6, < 0.8) + ffi (1.16.3) + fugit (1.9.0) + et-orbi (~> 1, >= 1.2.7) + raabro (~> 1.4) + globalid (1.2.1) + activesupport (>= 6.1) + haml (6.3.0) + temple (>= 0.8.2) + thor tilt haml-lint (0.999.999) haml_lint - haml_lint (0.37.1) - haml (>= 4.0, < 5.3) + haml_lint (0.55.0) + haml (>= 5.0) parallel (~> 1.10) rainbow - rubocop (>= 0.50.0) + rubocop (>= 1.0) sysexits (~> 1.1) - hashdiff (1.0.1) - hashie (4.1.0) - headless (2.3.1) + hashdiff (1.1.0) + hashie (5.0.0) highrise (3.2.3) activeresource (>= 3.2.13) hpricot (0.8.6) htmlentities (4.3.4) - i18n (1.8.10) + i18n (1.14.1) concurrent-ruby (~> 1.0) - i18n_data (0.13.0) - image_processing (1.12.1) + image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - influxdb-client (1.16.0) - influxdb-client-apis (1.16.0) - influxdb-client (= 1.16.0) - typhoeus (~> 1.0, >= 1.0.1) - jbuilder (2.11.2) + io-console (0.7.2) + irb (1.11.1) + rdoc + reline (>= 0.4.2) + jbuilder (2.11.5) + actionview (>= 5.0.0) activesupport (>= 5.0.0) - jmespath (1.4.0) - jquery-rails (4.4.0) + jmespath (1.6.2) + jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) jquery-ui-rails (6.0.1) railties (>= 3.2.16) - json-jwt (1.13.0) + json (2.7.1) + json-jwt (1.16.5) activesupport (>= 4.2) aes_key_wrap + base64 bindata - jwt (2.2.3) - kaminari (1.2.1) + faraday (~> 2.0) + faraday-follow_redirects + jwt (2.7.1) + kaminari (1.2.2) activesupport (>= 4.1.0) - kaminari-actionview (= 1.2.1) - kaminari-activerecord (= 1.2.1) - kaminari-core (= 1.2.1) - kaminari-actionview (1.2.1) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) actionview - kaminari-core (= 1.2.1) - kaminari-activerecord (1.2.1) + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) activerecord - kaminari-core (= 1.2.1) + kaminari-core (= 1.2.2) kaminari-bootstrap (3.0.1) kaminari (>= 0.13.0) rails - kaminari-core (1.2.1) - loofah (2.10.0) + kaminari-core (1.2.2) + language_server-protocol (3.17.0.3) + libv8-node (18.16.0.0) + libv8-node (18.16.0.0-x86_64-linux) + listen (3.8.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + loofah (2.22.0) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + mail (2.8.1) mini_mime (>= 0.1.1) - marcel (1.0.1) + net-imap + net-pop + net-smtp + marcel (1.0.2) + matrix (0.4.2) method_source (1.0.0) - mini_magick (4.11.0) - mini_mime (1.1.0) - mini_portile2 (2.6.1) - minitest (5.14.4) - minitest-reporters (1.4.3) + mini_magick (4.12.0) + mini_mime (1.1.5) + mini_portile2 (2.8.5) + mini_racer (0.8.0) + libv8-node (~> 18.16.0.0) + minitest (5.21.2) + minitest-reporters (1.6.1) ansi builder minitest (>= 5.0) ruby-progressbar - mocha (1.13.0) - msgpack (1.4.2) - multi_json (1.15.0) + mocha (2.1.0) + ruby2_keywords (>= 0.0.5) + msgpack (1.7.2) multi_xml (0.6.0) - multipart-post (2.1.1) + mutex_m (0.2.0) nested_form_fields (0.8.4) coffee-rails (>= 3.2.1) jquery-rails rails (>= 3.2.0) - net-ldap (0.17.0) - nio4r (2.5.8) - nokogiri (1.12.5) - mini_portile2 (~> 2.6.1) + net-http (0.4.1) + uri + net-imap (0.4.9.1) + date + net-protocol + net-ldap (0.19.0) + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.4.0.1) + net-protocol + nio4r (2.7.0) + nokogiri (1.16.0) + mini_portile2 (~> 2.8.2) racc (~> 1.4) - oauth2 (1.4.7) - faraday (>= 0.8, < 2.0) + nokogiri (1.16.0-x86_64-linux) + racc (~> 1.4) + oauth2 (2.0.9) + faraday (>= 0.17.3, < 3.0) jwt (>= 1.0, < 3.0) - multi_json (~> 1.3) multi_xml (~> 0.5) - rack (>= 1.2, < 3) - omniauth (2.0.4) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0) + version_gem (~> 1.1) + omniauth (2.1.2) hashie (>= 3.4.6) - rack (>= 1.6.2, < 3) + rack (>= 2.2.3) rack-protection - omniauth-keycloak (1.3.0) - json-jwt (~> 1.13.0) - omniauth (~> 2.0.4) + omniauth-keycloak (1.5.1) + faraday + json-jwt (> 1.13.0) + omniauth (>= 2.0) omniauth-oauth2 (~> 1.7.1) - omniauth-oauth2 (1.7.1) - oauth2 (~> 1.4) + omniauth-oauth2 (1.7.3) + oauth2 (>= 1.4, < 3) omniauth (>= 1.9, < 3) - omniauth-rails_csrf_protection (1.0.0) + omniauth-rails_csrf_protection (1.0.1) actionpack (>= 4.2) omniauth (~> 2.0) - omniauth-saml (2.0.0) + omniauth-saml (2.1.0) omniauth (~> 2.0) - ruby-saml (~> 1.9) + ruby-saml (~> 1.12) orm_adapter (0.5.0) - paper_trail (12.0.0) - activerecord (>= 5.2) - request_store (~> 1.1) - parallel (1.20.1) - parser (3.0.2.0) + paper_trail (15.1.0) + activerecord (>= 6.1) + request_store (~> 1.4) + parallel (1.24.0) + parser (3.3.0.5) ast (~> 2.4.1) + racc + pastel (0.8.0) + tty-color (~> 0.5) pdf-core (0.9.0) - pg (1.2.3) + pg (1.5.4) prawn (2.4.0) pdf-core (~> 0.9.0) ttfunk (~> 1.7) - prometheus_exporter (0.7.0) + prometheus_exporter (2.1.0) webrick protective (0.2.0) activerecord - pry (0.13.1) + pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - pry-byebug (3.9.0) + pry-byebug (3.10.1) byebug (~> 11.0) - pry (~> 0.13.0) - pry-doc (1.1.0) + pry (>= 0.13, < 0.15) + pry-doc (1.5.0) pry (~> 0.11) yard (~> 0.9.11) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (4.0.6) - puma (5.5.1) + psych (5.1.2) + stringio + public_suffix (5.0.4) + puma (6.4.2) nio4r (~> 2.0) - racc (1.5.2) - rack (2.2.3) - rack-protection (2.1.0) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (5.2.6) - actioncable (= 5.2.6) - actionmailer (= 5.2.6) - actionpack (= 5.2.6) - actionview (= 5.2.6) - activejob (= 5.2.6) - activemodel (= 5.2.6) - activerecord (= 5.2.6) - activestorage (= 5.2.6) - activesupport (= 5.2.6) - bundler (>= 1.3.0) - railties (= 5.2.6) - sprockets-rails (>= 2.0.0) + raabro (1.4.0) + racc (1.7.3) + rack (3.0.8) + rack-protection (4.0.0) + base64 (>= 0.1.0) + rack (>= 3.0.0, < 4) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails (7.1.3) + actioncable (= 7.1.3) + actionmailbox (= 7.1.3) + actionmailer (= 7.1.3) + actionpack (= 7.1.3) + actiontext (= 7.1.3) + actionview (= 7.1.3) + activejob (= 7.1.3) + activemodel (= 7.1.3) + activerecord (= 7.1.3) + activestorage (= 7.1.3) + activesupport (= 7.1.3) + bundler (>= 1.15.0) + railties (= 7.1.3) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-erd (1.6.1) + rails-erd (1.7.2) activerecord (>= 4.2) activesupport (>= 4.2) choice (~> 0.2.0) ruby-graphviz (~> 1.2) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - rails-i18n (5.1.3) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + rails-i18n (7.0.8) i18n (>= 0.7, < 2) - railties (>= 5.0, < 6) - rails_autolink (1.1.6) - rails (> 3.1) - railties (5.2.6) - actionpack (= 5.2.6) - activesupport (= 5.2.6) - method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) - rainbow (3.0.0) - rake (13.0.6) + railties (>= 6.0.0, < 8) + rails_autolink (1.1.8) + actionview (> 3.1) + activesupport (> 3.1) + railties (> 3.1) + railties (7.1.3) + actionpack (= 7.1.3) + activesupport (= 7.1.3) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.1.0) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) + ffi (~> 1.0) rb-readline (0.5.5) - rbtree3 (0.6.0) - rdoc (6.3.2) - regexp_parser (2.1.1) + rbtree3 (0.7.1) + rdoc (6.6.2) + psych (>= 4.0.0) + regexp_parser (2.9.0) + reline (0.4.2) + io-console (~> 0.5) request_profiler (0.0.4) ruby-prof - request_store (1.5.0) + request_store (1.5.1) rack (>= 1.4) - responders (3.0.1) - actionpack (>= 5.0) - railties (>= 5.0) - rexml (3.2.5) - rqrcode (2.0.0) + responders (3.1.1) + actionpack (>= 5.2) + railties (>= 5.2) + rexml (3.2.6) + rouge (4.2.0) + rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) - rqrcode_core (1.1.0) - rswag-ui (2.4.0) - actionpack (>= 3.1, < 7.0) - railties (>= 3.1, < 7.0) - rubocop (1.18.3) + rqrcode_core (1.2.0) + rswag-ui (2.13.0) + actionpack (>= 3.1, < 7.2) + railties (>= 3.1, < 7.2) + rubocop (1.60.1) + json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.0.0.0) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.7.0, < 2.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.7.0) - parser (>= 3.0.1.1) - rubocop-checkstyle_formatter (0.4.0) - rubocop (>= 0.35.1) - rubocop-minitest (0.14.0) - rubocop (>= 0.90, < 2.0) - rubocop-performance (1.11.4) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) - rubocop-rails (2.11.3) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.30.0) + parser (>= 3.2.1.0) + rubocop-checkstyle_formatter (0.6.0) + rubocop (>= 1.14.0) + rubocop-minitest (0.34.5) + rubocop (>= 1.39, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) + rubocop-performance (1.20.2) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) + rubocop-rails (2.23.1) activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 1.7.0, < 2.0) + rubocop (>= 1.33.0, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-graphviz (1.2.5) rexml - ruby-prof (1.4.3) - ruby-progressbar (1.11.0) - ruby-saml (1.12.2) - nokogiri (>= 1.10.5) + ruby-prof (1.7.0) + ruby-progressbar (1.13.0) + ruby-saml (1.16.0) + nokogiri (>= 1.13.10) rexml - ruby-vips (2.1.2) + ruby-vips (2.2.0) ffi (~> 1.12) - ruby2_keywords (0.0.4) + ruby2_keywords (0.0.5) rubyzip (2.3.2) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) @@ -493,85 +593,92 @@ GEM sprockets (> 3.0) sprockets-rails tilt - sdoc (2.2.0) + sdoc (2.6.1) rdoc (>= 5.0) seed-fu (2.3.9) activerecord (>= 3.1) activesupport (>= 3.1) selectize-rails (0.12.6) - selenium-webdriver (3.142.7) - childprocess (>= 0.5, < 4.0) - rubyzip (>= 1.2.2) + selenium-webdriver (4.8.0) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) sentry-raven (3.1.2) faraday (>= 1.0) - simplecov (0.21.2) - docile (~> 1.1) - simplecov-html (~> 0.11) - simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) - simplecov_json_formatter (0.1.3) - sixarm_ruby_unaccent (1.2.0) - sort_alphabetical (1.1.0) - unicode_utils (>= 1.2.2) - spring (2.1.1) - sprockets (4.0.2) + simpleidn (0.2.1) + unf (~> 0.1.4) + snaky_hash (2.0.1) + hashie + version_gem (~> 1.1, >= 1.1.1) + spring (4.1.3) + sprockets (4.2.1) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) sprockets (>= 3.0.0) + stringio (3.1.0) swagger-blocks (3.0.0) sysexits (1.2.0) - temple (0.8.2) - thor (1.1.0) - thread_safe (0.3.6) - tilt (2.0.10) - timeliness (0.4.4) + temple (0.10.3) + terser (1.2.0) + execjs (>= 0.3.0, < 3) + thor (1.3.0) + tilt (2.3.0) + timeliness (0.4.5) + timeout (0.4.1) ttfunk (1.7.0) + tty-color (0.6.0) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) - typhoeus (1.4.0) - ethon (>= 0.9.0) - tzinfo (1.2.9) - thread_safe (~> 0.1) - uglifier (4.2.0) - execjs (>= 0.3.0, < 3) - unicode-display_width (2.0.0) - unicode_utils (1.4.0) - uniform_notifier (1.14.2) - validates_by_schema (0.4.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unaccent (0.4.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.9.1) + unicode-display_width (2.5.0) + uniform_notifier (1.16.0) + uri (0.13.0) + validates_by_schema (0.5.1) activerecord (>= 5.0.0) - validates_timeliness (5.0.0) + validates_timeliness (7.0.0.beta2) + activemodel (>= 7.0.0, < 8) timeliness (>= 0.3.10, < 1) + version_gem (1.1.3) warden (1.2.9) rack (>= 2.0.9) - web-console (3.7.0) - actionview (>= 5.0) - activemodel (>= 5.0) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) bindex (>= 0.4.0) - railties (>= 5.0) - webdrivers (4.6.0) + railties (>= 6.0.0) + webdrivers (5.2.0) nokogiri (~> 1.6) rubyzip (>= 1.3.0) - selenium-webdriver (>= 3.0, < 4.0) - webmock (3.13.0) - addressable (>= 2.3.6) + selenium-webdriver (~> 4.0) + webmock (3.19.1) + addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.7.0) - websocket-driver (0.7.5) + webrick (1.8.1) + websocket (1.2.9) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - yard (0.9.26) + yard (0.9.34) + zeitwerk (2.6.12) PLATFORMS ruby + x86_64-linux DEPENDENCIES + activerecord-nulldb-adapter! acts_as_tree airbrake annotate @@ -586,34 +693,35 @@ DEPENDENCIES bullet bundler-audit cancancan - capybara chartjs-ror codez-tarantula coffee-rails config country_select + cuprite daemons dalli database_cleaner delayed_cron_job delayed_job_active_record devise + email_address execjs fabrication faker fast_jsonapi haml haml-lint - headless highrise image_processing - influxdb-client - influxdb-client-apis jbuilder jquery-rails jquery-ui-rails kaminari kaminari-bootstrap + listen + matrix + mini_racer minitest-reporters mocha nested_form_fields @@ -632,8 +740,9 @@ DEPENDENCIES pry-byebug pry-doc pry-rails + psych puma - rails (~> 5.2.x) + rails (~> 7.0.x) rails-controller-testing rails-erd rails-i18n @@ -654,11 +763,10 @@ DEPENDENCIES selectize-rails selenium-webdriver sentry-raven - simplecov-rcov! spring swagger-blocks + terser turbolinks - uglifier validates_by_schema validates_timeliness web-console @@ -666,4 +774,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.2.27 + 2.4.6 diff --git a/Rakefile b/Rakefile index e00d6110e..f220f6b00 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require File.expand_path('../config/application', __FILE__) +require File.expand_path('config/application', __dir__) unless Rails.env.production? require 'rails-erd' diff --git a/app/assets/images/qr-code-scan-icon.svg b/app/assets/images/qr-code-scan-icon.svg new file mode 100644 index 000000000..9f0068e83 --- /dev/null +++ b/app/assets/images/qr-code-scan-icon.svg @@ -0,0 +1 @@ +qr-code-scan \ No newline at end of file diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index de2ce2ae2..58053727c 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -23,7 +23,7 @@ #= require waypoints/shortcuts/sticky #= require waypoints/shortcuts/inview #= require modernizr-custom -#= require Chart.bundle.min +#= require Chart.min #= require chartjs-plugin-annotation.min #= require_self #= require_tree ./modules @@ -47,7 +47,6 @@ #= require meal_compensations #= require turbolinks - app = window.App ||= {} if typeof String.prototype.endsWith isnt 'function' diff --git a/app/assets/javascripts/modules/checkbox/toggler.js.coffee b/app/assets/javascripts/modules/checkbox/toggler.js.coffee index 80722be02..d06083723 100644 --- a/app/assets/javascripts/modules/checkbox/toggler.js.coffee +++ b/app/assets/javascripts/modules/checkbox/toggler.js.coffee @@ -24,6 +24,3 @@ class app.checkbox.Toggler selector = '[data-' + @data + ']' $(document).on('click', selector, (event) -> self.toggleChecked(this)) $(document).on('turbolinks:load', -> $(selector).each((i, e) -> self.toggleChecked(e))) - - - diff --git a/app/assets/stylesheets/_employees.scss b/app/assets/stylesheets/_employees.scss index 3c79ac828..73b6f6a17 100644 --- a/app/assets/stylesheets/_employees.scss +++ b/app/assets/stylesheets/_employees.scss @@ -7,6 +7,7 @@ .master-data-vcard-qrcode { display: block; width: 100px; + padding-bottom: 1em; @media (min-width: $screen-md-min) { padding-top: 20px; diff --git a/app/controllers/absences_controller.rb b/app/controllers/absences_controller.rb index aee6b736c..da5b10b7f 100644 --- a/app/controllers/absences_controller.rb +++ b/app/controllers/absences_controller.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. class AbsencesController < ManageController - self.permitted_attrs = [:name, :payed, :vacation] + self.permitted_attrs = %i[name payed vacation] end diff --git a/app/controllers/absencetimes_controller.rb b/app/controllers/absencetimes_controller.rb index 5f92df59c..13d81f89a 100644 --- a/app/controllers/absencetimes_controller.rb +++ b/app/controllers/absencetimes_controller.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. class AbsencetimesController < WorktimesController - self.permitted_attrs = [:absence_id, :report_type, :work_date, :hours, - :from_start_time, :to_end_time, :description] + self.permitted_attrs = %i[absence_id report_type work_date hours + from_start_time to_end_time description] before_render_form :set_accounts after_destroy :send_email_notification @@ -19,17 +21,17 @@ def create end def update - if entry.employee_id != @user.id - redirect_to index_path - else + if entry.employee_id == @user.id super + else + redirect_to index_path end end protected def create_multi_absence - @multiabsence = MultiAbsence.new + @multiabsence = Forms::MultiAbsence.new @multiabsence.employee = Employee.find_by(id: employee_id) @multiabsence.attributes = params[:absencetime] if @multiabsence.valid? @@ -39,7 +41,9 @@ def create_multi_absence else set_employees @create_multi = true - @multiabsence.worktime.errors.each do |attr, msg| + @multiabsence.worktime.errors.each do |error| + attr = error.attribute + msg = error.message entry.errors.add(attr, msg) end render 'new' @@ -50,7 +54,7 @@ def set_worktime_defaults @worktime.absence_id ||= params[:account_id] end - def set_accounts(_all = false) + def set_accounts @accounts = Absence.list end @@ -63,8 +67,8 @@ def check_has_accounting_post end def send_email_notification - if @worktime.employee != @user - ::EmployeeMailer.worktime_deleted_mail(@worktime, @user).deliver_now - end + return unless @worktime.employee != @user + + ::EmployeeMailer.worktime_deleted_mail(@worktime, @user).deliver_now end end diff --git a/app/controllers/accounting_posts_controller.rb b/app/controllers/accounting_posts_controller.rb index 74d31ac36..f6385df63 100644 --- a/app/controllers/accounting_posts_controller.rb +++ b/app/controllers/accounting_posts_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -10,7 +12,7 @@ class AccountingPostsController < CrudController :remaining_hours, :portfolio_item_id, :service_id, :billable, :description_required, :ticket_required, :from_to_times_required, :meal_compensation, - work_item_attributes: [:name, :shortname, :description]] + { work_item_attributes: %i[name shortname description] }] helper_method :order diff --git a/app/controllers/api/jsonapi_controller.rb b/app/controllers/api/jsonapi_controller.rb index 170b314fc..c8839c224 100644 --- a/app/controllers/api/jsonapi_controller.rb +++ b/app/controllers/api/jsonapi_controller.rb @@ -19,8 +19,8 @@ class JsonapiController < ManageController class IncludeError < StandardError def self.===(exception) - exception.class == ArgumentError && - exception.message.match(/is not specified as a relationship on/) + exception.instance_of?(ArgumentError) && + exception.message.include?('is not specified as a relationship on') end end @@ -65,15 +65,15 @@ def render_error(title, detail: nil, code: :error, status: 422, **opts) { id: request.uuid, status: status.to_s, - code: code, - title: title, - detail: detail + code:, + title:, + detail: }.merge(opts) ] } render json: error_payload, - status: status, + status:, content_type: Mime::Type.lookup_by_extension(:jsonapi) end @@ -82,7 +82,7 @@ def render_error(title, detail: nil, code: :error, status: 422, **opts) def set_pagination_headers response.headers.merge!( 'Pagination-Total-Count' => list_entries.total_count, - 'Pagination-Per-Page' => list_entries.current_per_page, + 'Pagination-Per-Page' => list_entries.limit_value, 'Pagination-Current-Page' => list_entries.current_page, 'Pagination-Total-Pages' => list_entries.total_pages ) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9ed695b4e..f5fbf2206 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2019, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -55,9 +57,9 @@ def after_sign_in_path_for(resource_or_scope) def set_period @period = nil p = session[:period] - if p.is_a? Array - @period = Period.new(*p) - end + return unless p.is_a? Array + + @period = Period.new(*p) end def sanitized_back_url @@ -66,7 +68,7 @@ def sanitized_back_url end def not_found - fail ActionController::RoutingError, 'Not Found' + raise ActionController::RoutingError, 'Not Found' end def set_sentry_request_context diff --git a/app/controllers/billing_addresses_controller.rb b/app/controllers/billing_addresses_controller.rb index 0207320e2..4a4401fee 100644 --- a/app/controllers/billing_addresses_controller.rb +++ b/app/controllers/billing_addresses_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -6,7 +8,7 @@ class BillingAddressesController < ManageController self.nesting = [Client] - self.permitted_attrs = [:contact_id, :supplement, :street, :zip_code, :town, :country] + self.permitted_attrs = %i[contact_id supplement street zip_code town country] before_render_form :set_contacts diff --git a/app/controllers/capacity_report_controller.rb b/app/controllers/capacity_report_controller.rb index 89173cd16..91acf592e 100644 --- a/app/controllers/capacity_report_controller.rb +++ b/app/controllers/capacity_report_controller.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +# frozen_string_literal: true # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 @@ -13,7 +13,7 @@ class CapacityReportController < ApplicationController def index if @period - report = ExtendedCapacityReport.new(@period) + report = Reports::ExtendedCapacityReport.new(@period) send_csv(report.to_csv, report.filename) else flash[:alert] = 'Bitte wählen Sie eine Zeitspanne für die Auslastung.' @@ -24,6 +24,6 @@ def index private def authorize_class - authorize!(:capacity_report, Evaluation) + authorize!(:capacity_report, Evaluations::Evaluation) end end diff --git a/app/controllers/clients_controller.rb b/app/controllers/clients_controller.rb index 6d7354412..38eb3f124 100644 --- a/app/controllers/clients_controller.rb +++ b/app/controllers/clients_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -7,7 +9,7 @@ class ClientsController < ManageController self.search_columns = ['work_items.name', 'work_items.shortname'] self.permitted_attrs = [:crm_key, :allow_local, :sector_id, :e_bill_account_key, - work_item_attributes: [:name, :shortname, :description]] + { work_item_attributes: %i[name shortname description] }] self.sort_mappings = { sector_id: 'sectors.name' } diff --git a/app/controllers/concerns/completable.rb b/app/controllers/concerns/completable.rb index 46ded925d..acb34d378 100644 --- a/app/controllers/concerns/completable.rb +++ b/app/controllers/concerns/completable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -26,20 +28,20 @@ module Completable private def validate_date - unless completion_dates.include?(entry_date) - entry.errors.add(completable_attr, 'ist nicht erlaubt') - throw(:abort) - end + return if completion_dates.include?(entry_date) + + entry.errors.add(completable_attr, 'ist nicht erlaubt') + throw(:abort) end def set_selected_month # Selecting a month in the future is fine, as an invalid selection # or even no selection ends up selecting the first (and most recent) # month. - if entry_date.present? - @selected_month = entry_date + 1.month - @selected_month = @selected_month.end_of_month - end + return if entry_date.blank? + + @selected_month = entry_date + 1.month + @selected_month = @selected_month.end_of_month end def set_dates diff --git a/app/controllers/concerns/csv_exportable.rb b/app/controllers/concerns/csv_exportable.rb index 651a28de8..542b12cae 100644 --- a/app/controllers/concerns/csv_exportable.rb +++ b/app/controllers/concerns/csv_exportable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -6,11 +8,11 @@ module CsvExportable def send_csv(csv_data, filename) csv_file_export_header(filename) - send_data(csv_data, filename: filename, type: 'text/csv; charset=utf-8; header=present') + send_data(csv_data, filename:, type: 'text/csv; charset=utf-8; header=present') end def csv_file_export_header(filename) - if request.env['HTTP_USER_AGENT'] =~ /msie/i + if /msie/i.match?(request.env['HTTP_USER_AGENT']) headers['Pragma'] = 'public' headers['Content-type'] = 'text/plain' headers['Cache-Control'] = 'no-cache, must-revalidate, post-check=0, pre-check=0' diff --git a/app/controllers/concerns/filterable.rb b/app/controllers/concerns/filterable.rb index 24a7b233a..05e22899c 100644 --- a/app/controllers/concerns/filterable.rb +++ b/app/controllers/concerns/filterable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/concerns/scopable.rb b/app/controllers/concerns/scopable.rb index e8b704fd2..9ed1d6573 100644 --- a/app/controllers/concerns/scopable.rb +++ b/app/controllers/concerns/scopable.rb @@ -9,9 +9,7 @@ module Scopable extend ActiveSupport::Concern included do - if respond_to?(:permitted_attrs=) - self.permitted_attrs = Array.wrap(permitted_attrs) + [:scope] - end + self.permitted_attrs = Array.wrap(permitted_attrs) + [:scope] if respond_to?(:permitted_attrs=) end private diff --git a/app/controllers/concerns/with_period.rb b/app/controllers/concerns/with_period.rb index a48e6aeab..2f830da01 100644 --- a/app/controllers/concerns/with_period.rb +++ b/app/controllers/concerns/with_period.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -22,12 +24,12 @@ def build_period Period.parse(params[:period_shortcut]) elsif build_start_end_period? Period.new(params[:start_date].presence, params[:end_date].presence).tap do |period| - fail ArgumentError, 'Start Datum nach End Datum' if period.negative? + raise ArgumentError, 'Start Datum nach End Datum' if period.negative? end end - rescue ArgumentError => ex + rescue ArgumentError => e # from Period.new or if period.negative? - flash.now[:alert] = "Ungültige Zeitspanne: #{ex}" + flash.now[:alert] = "Ungültige Zeitspanne: #{e}" params.delete(:start_date) params.delete(:end_date) params.delete(:period_shortcut) @@ -43,7 +45,7 @@ def build_shortcut_period? end def build_start_end_period? - allow_unlimited_period && (params[:start_date].present? || params[:end_date].present?) || - !allow_unlimited_period && params[:start_date].present? && params[:end_date].present? + (allow_unlimited_period && (params[:start_date].present? || params[:end_date].present?)) || + (!allow_unlimited_period && params[:start_date].present? && params[:end_date].present?) end end diff --git a/app/controllers/concerns/worktimes_csv.rb b/app/controllers/concerns/worktimes_csv.rb index 988d03d5e..a389955d9 100644 --- a/app/controllers/concerns/worktimes_csv.rb +++ b/app/controllers/concerns/worktimes_csv.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/concerns/worktimes_report.rb b/app/controllers/concerns/worktimes_report.rb index f4fb8b006..a0ac3a143 100644 --- a/app/controllers/concerns/worktimes_report.rb +++ b/app/controllers/concerns/worktimes_report.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -21,7 +23,7 @@ def combine_times combined_map = {} combined_times = [] @worktimes.each do |time| - if time.report_type.is_a?(StartStopType) && params[:start_stop] + if time.report_type.is_a?(ReportType::StartStopType) && params[:start_stop] combined_times.push time else combine_time(combined_map, combined_times, time) @@ -53,8 +55,8 @@ def combine_time(combined_map, combined_times, time) if combined_map.include?(key) combined_map[key].hours += time.hours if time.description.present? - if combined_map[key].description - combined_map[key].description += "\n" + time.description + if (description = combined_map[key].description) + combined_map[key].description = "#{description}\n#{time.description}" else combined_map[key].description = time.description end @@ -70,7 +72,7 @@ def combine_ticket(combined_tickets, employees, worktime) combined_tickets[:sum] += worktime.hours combine_ticket_employees(combined_tickets, employees, worktime) combine_ticket_date_range(combined_tickets[:date], worktime) - combined_tickets[:descriptions] << '"' + worktime.description + '"' if worktime.description? + combined_tickets[:descriptions] << %Q("#{worktime.description}") if worktime.description? end def combine_ticket_employees(combined_tickets, employees, worktime) diff --git a/app/controllers/configurations_controller.rb b/app/controllers/configurations_controller.rb index 496986fad..8f645d9f3 100644 --- a/app/controllers/configurations_controller.rb +++ b/app/controllers/configurations_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -6,6 +8,5 @@ class ConfigurationsController < ApplicationController skip_authorization_check - def index - end + def index; end end diff --git a/app/controllers/contacts_controller.rb b/app/controllers/contacts_controller.rb index 692f93a59..2e0dc0a16 100644 --- a/app/controllers/contacts_controller.rb +++ b/app/controllers/contacts_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -6,18 +8,18 @@ class ContactsController < ManageController self.nesting = [Client] - self.permitted_attrs = [:lastname, :firstname, :function, :email, :phone, :mobile, :crm_key] + self.permitted_attrs = %i[lastname firstname function email phone mobile crm_key] - self.search_columns = [:lastname, :firstname, :function, :email] + self.search_columns = %i[lastname firstname function email] def with_crm @client = Client.find_by!(work_item_id: params[:client_work_item_id]) @entries = @client.contacts.list.to_a - if Crm.instance - existing = @entries.collect(&:crm_key).compact - Crm.instance.find_client_contacts(@client).each do |c| - @entries << @client.contacts.new(c) unless existing.include?(c[:crm_key].to_s) - end + return unless Crm.instance + + existing = @entries.filter_map(&:crm_key) + Crm.instance.find_client_contacts(@client).each do |c| + @entries << @client.contacts.new(c) unless existing.include?(c[:crm_key].to_s) end end end diff --git a/app/controllers/contracts_controller.rb b/app/controllers/contracts_controller.rb index fb27e9658..76b4af496 100644 --- a/app/controllers/contracts_controller.rb +++ b/app/controllers/contracts_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/crud_controller.rb b/app/controllers/crud_controller.rb index 2099fb8c0..48665803b 100644 --- a/app/controllers/crud_controller.rb +++ b/app/controllers/crud_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -28,7 +30,7 @@ class CrudController < ListController helper_method :entry, :full_entry_label - prepend_before_action :entry, only: [:show, :new, :create, :edit, :update, :destroy] + prepend_before_action :entry, only: %i[show new create edit update destroy] # prepend_before_action :authenticate prepend_before_action :current_user @@ -37,8 +39,7 @@ class CrudController < ListController # Show one entry of this model. # GET /entries/1 # GET /entries/1.json - def show - end + def show; end # Display a form to create a new entry of this model. # GET /entries/new @@ -47,6 +48,10 @@ def new assign_attributes if params[model_identifier] end + # Display a form to edit an exisiting entry of this model. + # GET /entries/1/edit + def edit; end + # Create a new entry of this model from the passed params. # There are before and after create callbacks to hook into the action. # To customize the response, you may overwrite this action and call @@ -72,11 +77,6 @@ def create(options = {}) end end - # Display a form to edit an exisiting entry of this model. - # GET /entries/1/edit - def edit - end - # Update an existing entry of this model from the passed params. # There are before and after update callbacks to hook into the action. # To customize the response, you may overwrite this action and call @@ -198,7 +198,7 @@ def full_entry_label # Html safe error messages of the current entry. def error_messages - safe_join(entry.errors.full_messages, tag(:br)) + safe_join(entry.errors.full_messages, tag.br) end # json hash representation of an entry used for javascript responses diff --git a/app/controllers/departments_controller.rb b/app/controllers/departments_controller.rb index 27ecca571..59d528adf 100644 --- a/app/controllers/departments_controller.rb +++ b/app/controllers/departments_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/design_guide_controller.rb b/app/controllers/design_guide_controller.rb index 65a611169..8ca5d7f30 100644 --- a/app/controllers/design_guide_controller.rb +++ b/app/controllers/design_guide_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/dry_crud/generic_model.rb b/app/controllers/dry_crud/generic_model.rb index 320ac568c..949b67030 100644 --- a/app/controllers/dry_crud/generic_model.rb +++ b/app/controllers/dry_crud/generic_model.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -16,8 +18,6 @@ module GenericModel included do helper_method :model_class, :models_label, :path_args - private - delegate :model_class, :models_label, :model_identifier, to: 'self.class' end diff --git a/app/controllers/dry_crud/nestable.rb b/app/controllers/dry_crud/nestable.rb index d573479d3..0f3cc3b0e 100644 --- a/app/controllers/dry_crud/nestable.rb +++ b/app/controllers/dry_crud/nestable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -48,7 +50,7 @@ def load_optional_parent Array(optional_nesting).each do |clazz| key = clazz.name.underscore id = params["#{key}_id"] - if id && request.path =~ %r{\/#{key.pluralize}\/#{id}\/} + if id && request.path =~ %r{/#{key.pluralize}/#{id}/} parent = [parent_entry(clazz)] break end diff --git a/app/controllers/dry_crud/rememberable.rb b/app/controllers/dry_crud/rememberable.rb index 4c2cadcee..5427a03ea 100644 --- a/app/controllers/dry_crud/rememberable.rb +++ b/app/controllers/dry_crud/rememberable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -19,7 +21,7 @@ module Rememberable included do class_attribute :remember_params - self.remember_params = %w(q sort sort_dir page) + self.remember_params = %w[q sort sort_dir page] before_action :handle_remember_params, only: [:index] end diff --git a/app/controllers/dry_crud/render_callbacks.rb b/app/controllers/dry_crud/render_callbacks.rb index e795b0453..19c7ccb44 100644 --- a/app/controllers/dry_crud/render_callbacks.rb +++ b/app/controllers/dry_crud/render_callbacks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -17,13 +19,13 @@ module RenderCallbacks module Prepends # Helper method to run +before_render+ callbacks and render the action. # If a callback renders or redirects, the action is not rendered. - def render(*args, &block) - options = _normalize_render(*args, &block) + def render(*, &) + options = _normalize_render(*, &) callback = "render_#{options[:template]}" run_callbacks(callback) if respond_to?(:"_#{callback}_callbacks", true) - super(*args, &block) unless performed? + super unless performed? end private diff --git a/app/controllers/dry_crud/searchable.rb b/app/controllers/dry_crud/searchable.rb index dcdc12702..318c93d15 100644 --- a/app/controllers/dry_crud/searchable.rb +++ b/app/controllers/dry_crud/searchable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -30,10 +32,10 @@ def list_entries # Concat the word clauses with AND. def search_conditions - if params[:q].present? - search_word_conditions.reduce do |query, condition| - query.and(condition) - end + return if params[:q].blank? + + search_word_conditions.reduce do |query, condition| + query.and(condition) end end diff --git a/app/controllers/dry_crud/sortable.rb b/app/controllers/dry_crud/sortable.rb index 719087441..7789cce64 100644 --- a/app/controllers/dry_crud/sortable.rb +++ b/app/controllers/dry_crud/sortable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -42,7 +44,7 @@ def list_entries sortable = sortable?(params[:sort]) if sortable || default_sort clause = [sortable ? sort_expression : nil, default_sort] - super.reorder(clause.compact.join(', ')) + super.reorder(Arel.sql(clause.compact.join(', '))) else super end @@ -73,8 +75,8 @@ def sortable?(attr) def number_null_order? type = model_class.columns_hash[params[:sort]].try(:type) - !sort_mappings_with_indifferent_access.key?(params[:sort]) && - type == :integer || type == :float + (!sort_mappings_with_indifferent_access.key?(params[:sort]) && + type == :integer) || type == :float end end end diff --git a/app/controllers/employee_master_data_controller.rb b/app/controllers/employee_master_data_controller.rb index 60cb282a4..550605d71 100644 --- a/app/controllers/employee_master_data_controller.rb +++ b/app/controllers/employee_master_data_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -17,30 +19,29 @@ def model_class def index authorize!(:read, Employee) @employees = list_entries.to_a - if can?(:manage, Employment) - fetch_latest_employment_dates(@employees) - sort_by_latest_employment(@employees) - end + return unless can?(:manage, Employment) + + fetch_latest_employment_dates(@employees) + sort_by_latest_employment(@employees) end def show @employee = Employee.includes(current_employment: { - employment_roles_employments: [ - :employment_role, - :employment_role_level + employment_roles_employments: %i[ + employment_role + employment_role_level ] }) .find(params[:id]) authorize!(:read, @employee) - unless can?(:social_insurance, @employee) - @employee.social_insurance = nil - end + @employee.social_insurance = nil unless can?(:social_insurance, @employee) respond_to do |format| format.html - format.vcf - format.svg { render plain: qr_code.as_svg } + format.vcf { render plain: vcard } + format.svg { render plain: qr_code.as_svg(fill: 'fff') } + format.png { render plain: qr_code.as_png(fill: 'fff') } end end @@ -60,16 +61,16 @@ def list_entries def list_entries_includes(list) if can?(:manage, Employment) list.includes(:department, :employments, current_employment: { - employment_roles_employments: [ - :employment_role, - :employment_role_level + employment_roles_employments: %i[ + employment_role + employment_role_level ] }) else list.includes(:department, current_employment: { - employment_roles_employments: [ - :employment_role, - :employment_role_level + employment_roles_employments: %i[ + employment_role + employment_role_level ] }) end @@ -94,16 +95,21 @@ def get_latest_employment(employee) end def sort_by_latest_employment(list) - if params[:sort] == 'latest_employment' - list.sort! { |a, b| @employee_employment[a] <=> @employee_employment[b] } - if params[:sort_dir] == 'asc' - list.reverse! - end - end + return unless params[:sort] == 'latest_employment' + + list.sort! { |a, b| @employee_employment[a] <=> @employee_employment[b] } + return unless params[:sort_dir] == 'asc' + + list.reverse! + end + + def vcard(include: nil) + Employees::Vcard.new(@employee, include:).render end def qr_code - RQRCode::QRCode.new(employee_master_datum_url(id: params[:id], format: :vcf)) + vcf = vcard(include: %i[firstname lastname fullname phone_office phone_private email]) + RQRCode::QRCode.new(vcf) end # Must be included after the #list_entries method is defined. diff --git a/app/controllers/employees/log_controller.rb b/app/controllers/employees/log_controller.rb index f62dfb84e..6745983e5 100644 --- a/app/controllers/employees/log_controller.rb +++ b/app/controllers/employees/log_controller.rb @@ -10,7 +10,7 @@ class LogController < ApplicationController before_action :authorize_action def index - @presenter = LogPresenter.new(employee, params) + @presenter = Presenters::LogPresenter.new(employee, params) end private diff --git a/app/controllers/employees/omniauth_callbacks_controller.rb b/app/controllers/employees/omniauth_callbacks_controller.rb index d1aae90bb..853da4574 100644 --- a/app/controllers/employees/omniauth_callbacks_controller.rb +++ b/app/controllers/employees/omniauth_callbacks_controller.rb @@ -1,19 +1,27 @@ -class Employees::OmniauthCallbacksController < Devise::OmniauthCallbacksController - include OmniauthCallbacksHelper +# frozen_string_literal: true - def default - omni = request.env['omniauth.auth'] - authentication = Authentication.find_by(provider: omni['provider'], uid: omni['uid']) - if authentication - sign_in_user(authentication) - elsif current_user - add_new_oauth(authentication, omni) - else - login_with_matching_data(omni) +module Employees + class OmniauthCallbacksController < Devise::OmniauthCallbacksController + include OmniauthCallbacksHelper + + def default + omni = request.env['omniauth.auth'] + authentication = Authentication.find_by(provider: omni['provider'], uid: omni['uid']) + if authentication + sign_in_user(authentication) + elsif current_user + add_new_oauth(authentication, omni) + else + login_with_matching_data(omni) + end end - end - # TODO: Username wegspeichern - alias keycloakopenid default - alias saml default + # TODO: Username wegspeichern + alias keycloakopenid default + alias saml default + + def after_omniauth_failure_path_for(scope) + new_session_path(scope, prevent_auto_login: true) + end + end end diff --git a/app/controllers/employees/sessions_controller.rb b/app/controllers/employees/sessions_controller.rb new file mode 100644 index 000000000..bfc360ebd --- /dev/null +++ b/app/controllers/employees/sessions_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Copyright (c) 2006-2022, Puzzle ITC GmbH. This file is part of +# PuzzleTime and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/puzzle/puzzletime. + +module Employees + class SessionsController < Devise::SessionsController + helper_method :auto_redirect? + + private + + def no_local_auth? + !Settings.auth.db.active + end + + def omniauth_providers_active + Settings.auth&.omniauth&.map(&:second)&.map(&:active) + end + + def single_omniauth_provider? + omniauth_providers_active&.one? + end + + def auto_login_allowed? + return true unless (prevent = params[:prevent_auto_login]) + + !ActiveRecord::Type::Boolean.new.deserialize(prevent) + end + + def auto_redirect? + auto_login_allowed? && no_local_auth? && single_omniauth_provider? + end + end +end diff --git a/app/controllers/employees/worktimes_commit_controller.rb b/app/controllers/employees/worktimes_commit_controller.rb index 10aa2b588..4035d68b4 100644 --- a/app/controllers/employees/worktimes_commit_controller.rb +++ b/app/controllers/employees/worktimes_commit_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/employees/worktimes_review_controller.rb b/app/controllers/employees/worktimes_review_controller.rb index 13227201b..0cd67d277 100644 --- a/app/controllers/employees/worktimes_review_controller.rb +++ b/app/controllers/employees/worktimes_review_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/employees_controller.rb b/app/controllers/employees_controller.rb index 632ca39e3..7c338c410 100644 --- a/app/controllers/employees_controller.rb +++ b/app/controllers/employees_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -5,40 +7,40 @@ class EmployeesController < ManageController self.permitted_attrs = [:firstname, :lastname, :shortname, :email, :ldapname, - :department_id, :crm_key, :probation_period_end_date, + :department_id, :workplace_id, :crm_key, :probation_period_end_date, :graduation, :management, :phone_office, :phone_private, :street, :postal_code, :city, :birthday, :emergency_contact_name, :emergency_contact_phone, :marital_status, :social_insurance, :additional_information, :identity_card_type, :identity_card_valid_until, - nationalities: []] + { nationalities: [] }] - if Settings.employees.initial_vacation_days_editable - self.permitted_attrs += [:initial_vacation_days] - end + self.permitted_attrs += [:initial_vacation_days] if Settings.employees.initial_vacation_days_editable - self.search_columns = [:firstname, :lastname, :shortname] + self.search_columns = %i[firstname lastname shortname] self.sort_mappings = { department_id: 'departments.name' } def show - if Crm.instance.present? - person = Crm.instance.find_people_by_email(entry.email).first - if person - redirect_to Crm.instance.contact_url(person.id) - else - flash[:alert] = "Person mit Email '#{entry.email}' nicht gefunden in CRM." - end + return if Crm.instance.blank? + + person = Crm.instance.find_people_by_email(entry.email).first + if person + redirect_to Crm.instance.contact_url(person.id), allow_other_host: true + else + flash[:alert] = "Person mit Email '#{entry.email}' nicht gefunden in CRM." end end def settings + @employee = @user end def update_settings - attrs = (params[:user] && params.require(:user).permit(eval_periods: [])) || {} + @employee = @user + attrs = params.require(:employee).permit(:worktimes_commit_reminder, eval_periods: []) attrs[:eval_periods] = [] if attrs[:eval_periods].blank? - if @user.update_attributes(attrs) + if @employee.update(attrs) flash[:notice] = 'Die Benutzereinstellungen wurden aktualisiert' redirect_to root_path else @@ -46,26 +48,6 @@ def update_settings end end - def passwd - end - - # Update userpwd - def update_passwd - if @user.check_passwd(params[:pwd]) - if params[:change_pwd] == params[:change_pwd_confirmation] - @user.update_passwd!(params[:change_pwd]) - flash[:notice] = 'Das Passwort wurde aktualisiert' - redirect_to controller: 'evaluator' - else - flash.now[:notice] = 'Die Passwort Bestätigung stimmt nicht mit dem Passwort überein' - render 'passwd' - end - else - flash.now[:notice] = 'Das alte Passwort ist falsch' - render 'passwd' - end - end - private def list_entries diff --git a/app/controllers/employment_role_categories_controller.rb b/app/controllers/employment_role_categories_controller.rb index 866b71c23..5bdbf7f25 100644 --- a/app/controllers/employment_role_categories_controller.rb +++ b/app/controllers/employment_role_categories_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/employment_role_levels_controller.rb b/app/controllers/employment_role_levels_controller.rb index 2275ee416..d329417f0 100644 --- a/app/controllers/employment_role_levels_controller.rb +++ b/app/controllers/employment_role_levels_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/employment_roles_controller.rb b/app/controllers/employment_roles_controller.rb index 16211ca7d..9d0fa401d 100644 --- a/app/controllers/employment_roles_controller.rb +++ b/app/controllers/employment_roles_controller.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. class EmploymentRolesController < ManageController - self.permitted_attrs = [:name, :billable, :level, :employment_role_category_id] + self.permitted_attrs = %i[name billable level employment_role_category_id] def list_entries super.includes(:employment_role_category) diff --git a/app/controllers/employments_controller.rb b/app/controllers/employments_controller.rb index 68d1bcebf..6762e81ad 100644 --- a/app/controllers/employments_controller.rb +++ b/app/controllers/employments_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -9,13 +11,13 @@ class EmploymentsController < ManageController self.permitted_attrs = [ :percent, :start_date, :end_date, :vacation_days_per_year, :comment, - employment_roles_employments_attributes: [ - :id, - :employment_role_id, - :percent, - :employment_role_level_id, - :_destroy - ] + { employment_roles_employments_attributes: %i[ + id + employment_role_id + percent + employment_role_level_id + _destroy + ] } ] before_render_form :load_employment_roles @@ -26,8 +28,8 @@ class EmploymentsController < ManageController before_save :check_employment_role_uniqueness def list_entries - super.includes(employment_roles_employments: [:employment_role, - :employment_role_level]) + super.includes(employment_roles_employments: %i[employment_role + employment_role_level]) end private @@ -44,10 +46,10 @@ def prefill_from_newest_employment return if entry.persisted? || params[:employment].present? newest = parent.employments.list.first - if newest.present? - entry.percent = newest.percent - entry.employment_roles_employments = newest.employment_roles_employments.map(&:dup) - end + return if newest.blank? + + entry.percent = newest.percent + entry.employment_roles_employments = newest.employment_roles_employments.map(&:dup) end def check_percent @@ -57,22 +59,21 @@ def check_percent role_percent = employment_roles_employments .values .reject { |v| v[:_destroy] } - .collect { |v| v[:percent].to_f } - .sum + .sum { |v| v[:percent].to_f } - if entry.percent != role_percent - entry.errors.add(:percent, 'Funktionsanteile und Beschäftigungsgrad stimmen nicht überein.') - throw :abort - end + return unless entry.percent != role_percent + + entry.errors.add(:percent, 'Funktionsanteile und Beschäftigungsgrad stimmen nicht überein.') + throw :abort end def check_employment_role_uniqueness employment_role_ids = entry.employment_roles_employments .collect(&:employment_role_id) - if employment_role_ids.length != employment_role_ids.uniq.length - entry.errors.add(:employment_roles_employments, 'Funktionen können nicht doppelt erfasst werden.') - throw :abort - end + return unless employment_role_ids.length != employment_role_ids.uniq.length + + entry.errors.add(:employment_roles_employments, 'Funktionen können nicht doppelt erfasst werden.') + throw :abort end end diff --git a/app/controllers/evaluator_controller.rb b/app/controllers/evaluator_controller.rb index 75104e60c..90db63eb6 100644 --- a/app/controllers/evaluator_controller.rb +++ b/app/controllers/evaluator_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -11,6 +13,8 @@ class EvaluatorController < ApplicationController before_action :set_period + helper_method :search_conditions, :evaluation_type + def index overview end @@ -19,15 +23,14 @@ def overview set_navigation_levels @periods = init_periods @times = @periods.collect { |p| @evaluation.sum_times_grouped(p) } - if @evaluation.planned_hours - @plannings = @periods.collect { |p| @evaluation.sum_plannings_grouped(p) } - end + @plannings = @periods.collect { |p| @evaluation.sum_plannings_grouped(p) } if @evaluation.planned_hours @order = @evaluation.category.is_a?(WorkItem).presence && @evaluation.category.order render(overview_template) end def details + @absence = Absence.find_by(id: params[:absence_id]) if params[:absence_id] set_navigation_levels set_evaluation_details paginate_times @@ -63,56 +66,65 @@ def set_evaluation set_default_params set_default_evaluation - if @user.management && @evaluation.nil? - set_management_evaluation - end + set_management_evaluation if @user.management && @evaluation.nil? if @evaluation.nil? - @evaluation = EmployeeWorkItemsEval.new(@user.id) + @evaluation = Evaluations::EmployeeWorkItemsEval.new(@user.id) params[:evaluation] = 'userworkitems' end @evaluation end + def evaluation_type + params[:evaluation] + end + def set_default_evaluation - @evaluation = case params[:evaluation].downcase - when 'managed' then ManagedOrdersEval.new(@user) - when 'userworkitems' then EmployeeWorkItemsEval.new(@user.id) - when "employeesubworkitems#{@user.id}", 'usersubworkitems' then - params[:evaluation] = 'usersubworkitems' - EmployeeSubWorkItemsEval.new(params[:category_id], @user.id) - when 'userabsences' then EmployeeAbsencesEval.new(@user.id) - when 'subworkitems' then SubWorkItemsEval.new(params[:category_id]) - when 'workitememployees' then WorkItemEmployeesEval.new(params[:category_id]) - end + @evaluation = + case params[:evaluation].downcase + when 'managed' then Evaluations::ManagedOrdersEval.new(@user) + when 'userworkitems' then Evaluations::EmployeeWorkItemsEval.new(@user.id) + when "employeesubworkitems#{@user.id}", 'usersubworkitems' + params[:evaluation] = 'usersubworkitems' + Evaluations::EmployeeSubWorkItemsEval.new(params[:category_id], @user.id) + when 'userabsences' then Evaluations::EmployeeAbsencesEval.new( + @user.id, **search_conditions + ) + when 'subworkitems' then Evaluations::SubWorkItemsEval.new(params[:category_id]) + when 'workitememployees' then Evaluations::WorkItemEmployeesEval.new(params[:category_id]) + end end def set_default_params params[:evaluation] ||= 'userworkitems' case params[:evaluation].downcase - when 'employees' then + when 'employees' params[:department_id] = current_user.department_id unless params.key?(:department_id) end end def set_management_evaluation - @evaluation = case params[:evaluation].downcase - when 'clients' then ClientsEval.new - when 'employees' then EmployeesEval.new(params[:department_id]) - when 'departments' then DepartmentsEval.new - when 'clientworkitems' then ClientWorkItemsEval.new(params[:category_id]) - when 'employeeworkitems' then EmployeeWorkItemsEval.new(params[:category_id]) - when /employeesubworkitems(\d+)/ then - EmployeeSubWorkItemsEval.new(params[:category_id], Regexp.last_match[1]) - when 'departmentorders' then DepartmentOrdersEval.new(params[:category_id]) - when 'absences' then AbsencesEval.new - when 'employeeabsences' then EmployeeAbsencesEval.new(params[:category_id]) - end + @evaluation = + case params[:evaluation].downcase + when 'clients' then Evaluations::ClientsEval.new + when 'employees' then Evaluations::EmployeesEval.new(params[:department_id]) + when 'departments' then Evaluations::DepartmentsEval.new + when 'clientworkitems' then Evaluations::ClientWorkItemsEval.new(params[:category_id]) + when 'employeeworkitems' then Evaluations::EmployeeWorkItemsEval.new(params[:category_id]) + when /employeesubworkitems(\d+)/ then Evaluations::EmployeeSubWorkItemsEval.new( + params[:category_id], Regexp.last_match[1] + ) + when 'departmentorders' then Evaluations::DepartmentOrdersEval.new(params[:category_id]) + when 'absences' then Evaluations::AbsencesEval.new(**search_conditions) + when 'employeeabsences' then Evaluations::EmployeeAbsencesEval.new( + params[:category_id], **search_conditions + ) + end end def overview_template - if params[:evaluation] =~ /^userworkitems$|^employeeworkitems$/ + if /^userworkitems$|^employeeworkitems$/.match?(params[:evaluation]) 'overview_employee' elsif params[:evaluation] == 'employees' 'employees' @@ -129,9 +141,9 @@ def prepare_report_header def set_evaluation_details evaluation.set_division_id(params[:division_id]) - if params[:start_date].present? && params[:start_date] != '0' - @period = Period.new(params[:start_date], params[:end_date]) - end + return unless params[:start_date].present? && params[:start_date] != '0' + + @period = Period.new(params[:start_date], params[:end_date]) end def set_navigation_levels @@ -145,9 +157,7 @@ def set_navigation_levels def pop_level?(level, current) pop = level[0] == current[0] - if level[0] =~ /(employee|user)?subworkitems(\d*)/ - pop &&= level[1] == current[1] - end + pop &&= level[1] == current[1] if /(employee|user)?subworkitems(\d*)/.match?(level[0]) pop end @@ -156,11 +166,11 @@ def paginate_times .times(@period) .includes(:employee, :work_item) .page(params[:page]) - if @evaluation.absences - @times = @times.includes(:absence) - else - @times = @times.includes(:invoice) - end + @times = if @evaluation.absences + @times.includes(:absence) + else + @times.includes(:invoice) + end @times end @@ -181,19 +191,25 @@ def init_periods if @period [@period] else - @user.eval_periods. - collect { |p| Period.parse(p) }. - sort_by do |p| - [p.nil? || p.unlimited? ? 999_999 : p.length.round(-1), - p.try(:start_date) || Time.zone.today] - end + @user.eval_periods + .collect { |p| Period.parse(p) } + .sort_by do |p| + [p.nil? || p.unlimited? ? 999_999 : p.length.round(-1), + p.try(:start_date) || Time.zone.today] + end end end + def search_conditions + return {} if params[:absence_id].blank? + + { absence_id: params[:absence_id] } + end + def authorize_action params[:evaluation] ||= params[:action].to_s evaluation action = params[:evaluation].gsub(/\d+$/, '').to_sym - authorize!(action, Evaluation) + authorize!(action, Evaluations::Evaluation) end end diff --git a/app/controllers/expenses_controller.rb b/app/controllers/expenses_controller.rb index 39a9f047e..68ab3afe4 100644 --- a/app/controllers/expenses_controller.rb +++ b/app/controllers/expenses_controller.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + class ExpensesController < ManageController include Filterable self.optional_nesting = [Employee] - self.permitted_attrs = [:payment_date, :employee_id, :kind, :order_id, :description, :amount, :receipt] - self.remember_params += %w(status employee_id reimbursement_date department_id) + self.permitted_attrs = %i[payment_date employee_id kind order_id description amount receipt] + self.remember_params += %w[status employee_id reimbursement_date department_id] before_render_index :populate_management_filter_selects, unless: :parent before_render_index :populate_employee_filter_selects, if: :parent @@ -12,10 +14,6 @@ class ExpensesController < ManageController before_save :attach_resized_receipt - def new - entry.attributes = template_attributes - end - def index if parent respond_to do |format| @@ -27,6 +25,10 @@ def index end end + def new + entry.attributes = template_attributes + end + def update with_protected_approved_state do options = params[:review] ? { location: expenses_review_path(entry) } : {} @@ -118,15 +120,16 @@ def receipt_param def attach_resized_receipt return unless receipt_param - resized = ImageProcessing::MiniMagick + resized = ImageProcessing::Vips .source(receipt_param.tempfile) .resize_to_limit(Settings.expenses.receipt.max_pixel, Settings.expenses.receipt.max_pixel) .saver(quality: Settings.expenses.receipt.quality) .convert('jpg') + .loader(page: 0) .call target_filename = "#{File.basename(receipt_param.original_filename.to_s, '.*')}.jpg" - entry.receipt.attach(io: File.open(resized), filename: target_filename, content_type: 'image/jpg') + entry.receipt.attach(io: File.open(resized), filename: target_filename, content_type: 'image/jpeg') end end diff --git a/app/controllers/expenses_reviews_controller.rb b/app/controllers/expenses_reviews_controller.rb index c8b59f3de..507bb5184 100644 --- a/app/controllers/expenses_reviews_controller.rb +++ b/app/controllers/expenses_reviews_controller.rb @@ -8,12 +8,12 @@ class ExpensesReviewsController < ManageController helper_method :model_class, :controller_class, :review_list - def self.model_class; Expense; end + def self.model_class = Expense - def self.controller_class; Expense; end + def self.controller_class = Expense - self.permitted_attrs = [:payment_date, :employee_id, :kind, :order_id, :description, :amount, :receipt] - self.remember_params += %w(status employee_id reimbursement_date department_id) + self.permitted_attrs = %i[payment_date employee_id kind order_id description amount receipt] + self.remember_params += %w[status employee_id reimbursement_date department_id] skip_authorize_resource before_action :authorize @@ -28,9 +28,9 @@ def index end def show - unless entry.pending? || entry.deferred? - redirect_to expenses_path(returning: true), notice: "#{entry} wurde bereits bearbeitet" - end + return if entry.pending? || entry.deferred? + + redirect_to expenses_path(returning: true), notice: "#{entry} wurde bereits bearbeitet" end def update diff --git a/app/controllers/export_report_controller.rb b/app/controllers/export_report_controller.rb index fc8b6084c..1d4f26442 100644 --- a/app/controllers/export_report_controller.rb +++ b/app/controllers/export_report_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -26,13 +28,13 @@ def index private def authorize_class - authorize!(:export_report, Evaluation) + authorize!(:export_report, Evaluations::Evaluation) end def report case params[:report] - when 'role_distribution' then RoleDistributionReport - when 'overtime_vacations' then OvertimeVacationsReport + when 'role_distribution' then Reports::RoleDistributionReport + when 'overtime_vacations' then Reports::OvertimeVacationsReport end end end diff --git a/app/controllers/holidays_controller.rb b/app/controllers/holidays_controller.rb index 310484b4f..bbc97d7b3 100644 --- a/app/controllers/holidays_controller.rb +++ b/app/controllers/holidays_controller.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. class HolidaysController < ManageController - self.permitted_attrs = [:holiday_date, :musthours_day] + self.permitted_attrs = %i[holiday_date musthours_day] end diff --git a/app/controllers/invoices_controller.rb b/app/controllers/invoices_controller.rb index 445b2bc99..891e52ede 100644 --- a/app/controllers/invoices_controller.rb +++ b/app/controllers/invoices_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -7,13 +9,13 @@ class InvoicesController < CrudController self.nesting = [Order] self.permitted_attrs = [:billing_date, :due_date, :period_from, :period_to, - :billing_address_id, :grouping, employee_ids: [], work_item_ids: []] + :billing_address_id, :grouping, { employee_ids: [], work_item_ids: [] }] self.sort_mappings = { period: :period_from, manual?: :grouping } helper_method :checked_work_item_ids, :checked_employee_ids, :order - prepend_before_action :entry, only: [:show, :new, :create, :edit, :update, :destroy, :sync] + prepend_before_action :entry, only: %i[show new create edit update destroy sync] before_render_index :load_totals_paid before_render_form :load_associations @@ -23,15 +25,13 @@ def show format.html format.json format.pdf do - if Invoicing.instance - pdf = Invoicing.instance.get_pdf(entry) - send_data(pdf, - filename: "#{entry.reference}.pdf", - type: 'application/pdf', - disposition: :inline) - else - fail ActionController::UnknownFormat - end + raise ActionController::UnknownFormat unless Invoicing.instance + + pdf = Invoicing.instance.get_pdf(entry) + send_data(pdf, + filename: "#{entry.reference}.pdf", + type: 'application/pdf', + disposition: :inline) end end end @@ -111,12 +111,10 @@ def copy_attrs_from_params(attrs) attrs[:period_from] ||= params[:start_date] attrs[:period_to] ||= params[:end_date] attrs[:grouping] = 'manual' if params[:manual_invoice] - if params[:employee_id].present? - attrs[:employee_ids] = Array(attrs[:employee_ids]) << params[:employee_id] - end - if params[:work_item_id].present? - attrs[:work_item_ids] = Array(attrs[:work_item_ids]) << params[:work_item_id] - end + attrs[:employee_ids] = Array(attrs[:employee_ids]) << params[:employee_id] if params[:employee_id].present? + return if params[:work_item_id].blank? + + attrs[:work_item_ids] = Array(attrs[:work_item_ids]) << params[:work_item_id] end def init_default_attrs(attrs) @@ -128,10 +126,10 @@ def init_default_attrs(attrs) attrs[:employee_ids] = employees_for_period(attrs[:period_from], attrs[:period_to]).map(&:id) end - if attrs[:work_item_ids].blank? - attrs[:work_item_ids] = work_items_for_period(attrs[:period_from], - attrs[:period_to]).map(&:id) - end + return if attrs[:work_item_ids].present? + + attrs[:work_item_ids] = work_items_for_period(attrs[:period_from], + attrs[:period_to]).map(&:id) end def order @@ -205,7 +203,7 @@ def billing_date end def due_date - entry.due_date || billing_date + payment_period.days if payment_period.present? + entry.due_date || (billing_date + payment_period.days) if payment_period.present? end def last_grouping diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 5dbaa4e6c..fc962deb8 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/manage_controller.rb b/app/controllers/manage_controller.rb index 627b0f428..958368159 100644 --- a/app/controllers/manage_controller.rb +++ b/app/controllers/manage_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/meal_compensations_controller.rb b/app/controllers/meal_compensations_controller.rb index d15c47869..09a65b7bd 100644 --- a/app/controllers/meal_compensations_controller.rb +++ b/app/controllers/meal_compensations_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2020, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -36,7 +38,7 @@ def details private def authorize_class - authorize!(:meal_compensation, Evaluation) + authorize!(:meal_compensation, Evaluations::Evaluation) end def default_period diff --git a/app/controllers/multi_worktimes_controller.rb b/app/controllers/multi_worktimes_controller.rb index e726d42e5..12dbbed40 100644 --- a/app/controllers/multi_worktimes_controller.rb +++ b/app/controllers/multi_worktimes_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -41,7 +43,7 @@ def worktimes @worktimes ||= Worktime .where(id: params[:worktime_ids]) - .includes([:employee, :work_item, :invoice]) + .includes(%i[employee work_item invoice]) end def load_field_presets @@ -57,14 +59,14 @@ def multi_worktime_value(attr) end def changed_attrs - @changed_attrs ||= %w(work_item_id ticket billable).select do |attr| + @changed_attrs ||= %w[work_item_id ticket billable].select do |attr| ActiveRecord::Type::Boolean.new.deserialize(params["change_#{attr}"]) end end def update_worktimes Worktime.transaction do - worktimes.includes(work_item: :accounting_post).each do |t| + worktimes.includes(work_item: :accounting_post).find_each do |t| # update each individually to run validations t.update!(params.permit(*changed_attrs)) end diff --git a/app/controllers/order_comments_controller.rb b/app/controllers/order_comments_controller.rb index b450e11b4..1f640c1b6 100644 --- a/app/controllers/order_comments_controller.rb +++ b/app/controllers/order_comments_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/order_controlling_controller.rb b/app/controllers/order_controlling_controller.rb index de09a98ad..aa4358f96 100644 --- a/app/controllers/order_controlling_controller.rb +++ b/app/controllers/order_controlling_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/order_kinds_controller.rb b/app/controllers/order_kinds_controller.rb index 42ae4691d..1986ad5be 100644 --- a/app/controllers/order_kinds_controller.rb +++ b/app/controllers/order_kinds_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/order_plannings_controller.rb b/app/controllers/order_plannings_controller.rb index bbecca8d7..a2cdb61aa 100644 --- a/app/controllers/order_plannings_controller.rb +++ b/app/controllers/order_plannings_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/order_reports_controller.rb b/app/controllers/order_reports_controller.rb index ac6b0f635..7fd167935 100644 --- a/app/controllers/order_reports_controller.rb +++ b/app/controllers/order_reports_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -7,10 +9,10 @@ class OrderReportsController < ApplicationController include DryCrud::Rememberable include WithPeriod - self.remember_params = %w(start_date end_date department_id + self.remember_params = %w[start_date end_date department_id client_work_item_id category_work_item_id kind_id status_id responsible_id target major_chance_value - major_risk_value) + major_risk_value] before_action :authorize_class @@ -37,9 +39,7 @@ def set_filter_values @departments = Department.list @clients = WorkItem.joins(:client).list @categories = [] - if params[:client_work_item_id].present? - @categories = WorkItem.find(params[:client_work_item_id]).categories.list - end + @categories = WorkItem.find(params[:client_work_item_id]).categories.list if params[:client_work_item_id].present? @order_kinds = OrderKind.list @order_status = OrderStatus.list @order_responsibles = Employee.joins(:managed_orders).distinct.list diff --git a/app/controllers/order_services_controller.rb b/app/controllers/order_services_controller.rb index 7a071c317..b127787f6 100644 --- a/app/controllers/order_services_controller.rb +++ b/app/controllers/order_services_controller.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. class OrderServicesController < ApplicationController - EMPTY = '[leer]'.freeze + EMPTY = '[leer]' EMPTY_TICKET = EMPTY EMPTY_INVOICE = OpenStruct.new(id: EMPTY, reference: EMPTY) MAX_ENTRIES = 250 @@ -15,8 +17,8 @@ class OrderServicesController < ApplicationController include WorktimesReport include WorktimesCsv - self.remember_params = %w(start_date end_date period_shortcut employee_id work_item_id ticket - billable invoice_id) + self.remember_params = %w[start_date end_date period_shortcut employee_id work_item_id ticket + billable invoice_id] before_action :order before_action :authorize_class @@ -48,10 +50,10 @@ def report private def list_worktimes(period) - entries = order.worktimes. - includes(:employee, :work_item). - order(:work_date). - in_period(period) + entries = order.worktimes + .includes(:employee, :work_item) + .order(:work_date) + .in_period(period) entries = filter_entries_allow_empty_by(entries, EMPTY, :ticket, :invoice_id) filter_entries_by(entries, :employee_id, :work_item_id, :billable, :meal_compensation) end @@ -73,7 +75,7 @@ def set_filter_values end def prepare_report_header - work_item_id = params[:work_item_id].present? ? params[:work_item_id] : order.work_item_id + work_item_id = params[:work_item_id].presence || order.work_item_id @work_item = WorkItem.find(work_item_id) @employee = Employee.find(params[:employee_id]) if params[:employee_id].present? set_period_with_invoice diff --git a/app/controllers/order_statuses_controller.rb b/app/controllers/order_statuses_controller.rb index 9ae84deb2..74c7b37f0 100644 --- a/app/controllers/order_statuses_controller.rb +++ b/app/controllers/order_statuses_controller.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. class OrderStatusesController < ManageController - self.permitted_attrs = [:name, :style, :closed, :position, :default] + self.permitted_attrs = %i[name style closed position default] end diff --git a/app/controllers/order_targets_controller.rb b/app/controllers/order_targets_controller.rb index ac306b20f..e7d04f45e 100644 --- a/app/controllers/order_targets_controller.rb +++ b/app/controllers/order_targets_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -8,8 +10,7 @@ class OrderTargetsController < ApplicationController before_action :authorize_class before_action :set_order_targets - def show - end + def show; end def update update_targets @@ -23,7 +24,7 @@ def update_targets @errors = OrderTarget.new.errors @order_targets.each do |target| unless target.update(target_params(target)) - target.errors.each { |attr, msg| @errors.add(attr, msg) } + target.errors.each { |error| @errors.add(error.attribute, error.message) } end end end diff --git a/app/controllers/order_uncertainties_controller.rb b/app/controllers/order_uncertainties_controller.rb index 18f67f4e0..b67d7d8d7 100644 --- a/app/controllers/order_uncertainties_controller.rb +++ b/app/controllers/order_uncertainties_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -5,7 +7,7 @@ class OrderUncertaintiesController < CrudController self.nesting = Order - self.permitted_attrs = [:name, :probability, :impact, :measure] + self.permitted_attrs = %i[name probability impact measure] helper_method :index_path diff --git a/app/controllers/orders/committed_controller.rb b/app/controllers/orders/committed_controller.rb index 474f0b7bf..57fe79e2e 100644 --- a/app/controllers/orders/committed_controller.rb +++ b/app/controllers/orders/committed_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/orders/completed_controller.rb b/app/controllers/orders/completed_controller.rb index 817ed82e8..7f379c06a 100644 --- a/app/controllers/orders/completed_controller.rb +++ b/app/controllers/orders/completed_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb index 4355d9f78..3abafa2ff 100644 --- a/app/controllers/orders_controller.rb +++ b/app/controllers/orders_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -8,13 +10,13 @@ class OrdersController < CrudController self.permitted_attrs = [ :crm_key, :kind_id, :responsible_id, :department_id, :status_id, - work_item_attributes: [:name, :shortname, :description, :parent_id], - order_team_members_attributes: [:id, :employee_id, :comment, :_destroy], - order_contacts_attributes: [:id, :contact_id_or_crm, :comment, :_destroy], - additional_crm_orders_attributes: [:id, :crm_key, :_destroy] + { work_item_attributes: %i[name shortname description parent_id], + order_team_members_attributes: %i[id employee_id comment _destroy], + order_contacts_attributes: %i[id contact_id_or_crm comment _destroy], + additional_crm_orders_attributes: %i[id crm_key _destroy] } ] - self.remember_params += %w(department_id kind_id status_id responsible_id) + self.remember_params += %w[department_id kind_id status_id responsible_id] self.sort_mappings = { client: 'work_items.path_names', @@ -25,7 +27,7 @@ class OrdersController < CrudController status: 'order_statuses.position' } - self.search_columns = %w(work_items.path_shortnames work_items.path_names) + self.search_columns = %w[work_items.path_shortnames work_items.path_names] before_action :set_filter_values, only: :index @@ -60,9 +62,7 @@ def crm_load @crm = Crm.instance @order = Order.find_by(crm_key: key) @crm_order = @crm.find_order(key) - if @crm_order - @client = Client.where(crm_key: @crm_order[:client][:key].to_s).first - end + @client = Client.where(crm_key: @crm_order[:client][:key].to_s).first if @crm_order rescue Crm::Error => e @crm_error = e.message end @@ -112,7 +112,7 @@ def handle_remember_params end def filter_params_present? - (params.keys & %w(department_id kind_id status_id responsible_id)).present? + params.keys.intersect?(%w[department_id kind_id status_id responsible_id]) end def set_default_filter_params @@ -143,11 +143,11 @@ def build_entry def assign_attributes super - if entry.new_record? - entry.work_item.parent_id ||= (params[:category_active] && - params[:category_work_item_id].presence) || - params[:client_work_item_id].presence - end + return unless entry.new_record? + + entry.work_item.parent_id ||= (params[:category_active] && + params[:category_work_item_id].presence) || + params[:client_work_item_id].presence end def copy_associations @@ -174,7 +174,7 @@ def load_responsibles .joins(:managed_orders) .employed_ones(Period.current_year) .select('employees.*, ' \ - "CASE WHEN employees.id = #{current_user.id.to_s(:db)} THEN 1 " \ + "CASE WHEN employees.id = #{current_user.id.to_fs(:db)} THEN 1 " \ 'ELSE 2 END AS employee_order') # current user should be on top .reorder('employee_order, lastname, firstname') end @@ -191,7 +191,7 @@ def set_option_values def load_client_options clients = Client.list - if Crm.instance && Crm.instance.restrict_local? + if Crm.instance&.restrict_local? clients = clients.where(allow_local: true).to_a if params[:client_work_item_id].present? client = Client.find_by(work_item_id: params[:client_work_item_id]) @@ -215,9 +215,7 @@ def load_contact_options def append_crm_contacts(contacts) entry.order_contacts.each do |oc| - if oc.contact && oc.contact.id.nil? - contacts << oc.contact - end + contacts << oc.contact if oc.contact && oc.contact.id.nil? end contacts end diff --git a/app/controllers/ordertimes_controller.rb b/app/controllers/ordertimes_controller.rb index a04e4350e..49854214a 100644 --- a/app/controllers/ordertimes_controller.rb +++ b/app/controllers/ordertimes_controller.rb @@ -1,20 +1,22 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. class OrdertimesController < WorktimesController - self.permitted_attrs = [:account_id, :report_type, :work_date, :hours, :meal_compensation, - :from_start_time, :to_end_time, :description, :billable, :ticket] + self.permitted_attrs = %i[account_id report_type work_date hours meal_compensation + from_start_time to_end_time description billable ticket] after_destroy :send_email_notification def update - if entry.employee_id != @user.id + if entry.employee_id == @user.id + super + else build_splitable create_part - else - super end end @@ -84,7 +86,7 @@ def splitable end def build_splitable - @split = session[:split] = WorktimeEdit.new(entry) + @split = session[:split] = Forms::WorktimeEdit.new(entry) end def build_worktime @@ -93,10 +95,10 @@ def build_worktime end def send_email_notification - if worktime_employee? - ::EmployeeMailer.worktime_deleted_mail(@worktime, @user).deliver_now - flash[:warning] = - "#{@worktime.employee} wurde per E-Mail darüber informiert, dass du diesen Eintrag gelöscht hast." - end + return unless worktime_employee? + + ::EmployeeMailer.worktime_deleted_mail(@worktime, @user).deliver_now + flash[:warning] = + "#{@worktime.employee} wurde per E-Mail darüber informiert, dass du diesen Eintrag gelöscht hast." end end diff --git a/app/controllers/overtime_vacations_controller.rb b/app/controllers/overtime_vacations_controller.rb index 9ba39f03e..85fab9653 100644 --- a/app/controllers/overtime_vacations_controller.rb +++ b/app/controllers/overtime_vacations_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -6,7 +8,7 @@ class OvertimeVacationsController < ManageController self.nesting = Employee - self.permitted_attrs = [:hours, :transfer_date] + self.permitted_attrs = %i[hours transfer_date] def show redirect_to employee_overtime_vacations_path(entry.employee) diff --git a/app/controllers/periods_controller.rb b/app/controllers/periods_controller.rb index b65fb23f5..204aca5ac 100644 --- a/app/controllers/periods_controller.rb +++ b/app/controllers/periods_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PeriodsController < ApplicationController skip_authorization_check @@ -9,13 +11,13 @@ def show def update @period = period_from_params - fail ArgumentError, 'Start Datum nach End Datum' if @period.negative? + raise ArgumentError, 'Start Datum nach End Datum' if @period.negative? session[:period] = [@period.start_date.to_s, @period.end_date.to_s, @period.label] # redirect_to_overview redirect_to sanitized_back_url - rescue ArgumentError => ex # ArgumentError from Period.new or if period.negative? - flash[:alert] = "Ungültige Zeitspanne: #{ex}" + rescue ArgumentError => e # ArgumentError from Period.new or if period.negative? + flash[:alert] = "Ungültige Zeitspanne: #{e}" render action: 'show' end diff --git a/app/controllers/plannings/base_controller.rb b/app/controllers/plannings/base_controller.rb index b57dc6c96..be056860e 100644 --- a/app/controllers/plannings/base_controller.rb +++ b/app/controllers/plannings/base_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -66,7 +68,7 @@ def plannings_to_destroy def params_with_restricted_items items = params[:items] || [] items = items.values if items.is_a?(Hash) || items.is_a?(ActionController::Parameters) - { items: items, + { items:, planning: params[:planning] || {} } end diff --git a/app/controllers/plannings/companies_controller.rb b/app/controllers/plannings/companies_controller.rb index 49ed42daa..5f5321a9e 100644 --- a/app/controllers/plannings/companies_controller.rb +++ b/app/controllers/plannings/companies_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/controllers/plannings/custom_lists_controller.rb b/app/controllers/plannings/custom_lists_controller.rb index 33fd4af39..53fd85507 100644 --- a/app/controllers/plannings/custom_lists_controller.rb +++ b/app/controllers/plannings/custom_lists_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -6,7 +8,7 @@ module Plannings class CustomListsController < CrudController self.nesting = [:plannings] - self.permitted_attrs = [:name, :item_type, item_ids: []] + self.permitted_attrs = [:name, :item_type, { item_ids: [] }] self.search_columns = [:name] before_render_show :set_items @@ -41,9 +43,10 @@ def model_scope def model_params super.tap do |p| - if p[:item_type] && ![Employee, Order].map(&:sti_name).include?(p[:item_type]) - raise ActionController::BadRequest - end + return p if p[:item_type].blank? + return p if [Employee, Order].map(&:sti_name).include?(p[:item_type]) + + raise ActionController::BadRequest end end end diff --git a/app/controllers/plannings/departments_controller.rb b/app/controllers/plannings/departments_controller.rb index 3651e840e..687471eb4 100644 --- a/app/controllers/plannings/departments_controller.rb +++ b/app/controllers/plannings/departments_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -5,7 +7,7 @@ module Plannings class DepartmentsController < ListController - self.search_columns = %w(name shortname) + self.search_columns = %w[name shortname] private diff --git a/app/controllers/plannings/employees_controller.rb b/app/controllers/plannings/employees_controller.rb index c8e1d3487..af9533e4f 100644 --- a/app/controllers/plannings/employees_controller.rb +++ b/app/controllers/plannings/employees_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -5,11 +7,11 @@ module Plannings class EmployeesController < BaseController - self.search_columns = [:firstname, :lastname, :shortname] + self.search_columns = %i[firstname lastname shortname] skip_authorize_resource - before_action :load_possible_work_items, only: [:new, :show] + before_action :load_possible_work_items, only: %i[new show] private @@ -27,10 +29,11 @@ def build_board end def load_possible_work_items - @possible_work_items ||= WorkItem - .joins(:accounting_post) - .where(closed: false) - .list + @possible_work_items ||= + WorkItem + .joins(:accounting_post) + .where(closed: false) + .list end def params_with_restricted_items diff --git a/app/controllers/plannings/multi_employees_controller.rb b/app/controllers/plannings/multi_employees_controller.rb index b31f2df7a..954b6a608 100644 --- a/app/controllers/plannings/multi_employees_controller.rb +++ b/app/controllers/plannings/multi_employees_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -16,17 +18,15 @@ def show private def employees - @employees ||= begin - if params[:department_id] - d = Department.find(params[:department_id]) - @title = "Planung der Members von #{d}" - d.employees.employed_ones(@period).list - elsif params[:custom_list_id] - CustomList.where(item_type: Employee.sti_name).find(params[:custom_list_id]).items.list - else - raise ActiveRecord::RecordNotFound - end - end + @employees ||= if params[:department_id] + d = Department.find(params[:department_id]) + @title = "Planung der Members von #{d}" + d.employees.employed_ones(@period).list + elsif params[:custom_list_id] + CustomList.where(item_type: Employee.sti_name).find(params[:custom_list_id]).items.list + else + raise ActiveRecord::RecordNotFound + end end def employee diff --git a/app/controllers/plannings/multi_orders_controller.rb b/app/controllers/plannings/multi_orders_controller.rb index 6bf3be2e1..1411a523a 100644 --- a/app/controllers/plannings/multi_orders_controller.rb +++ b/app/controllers/plannings/multi_orders_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -16,17 +18,15 @@ def show private def orders - @orders ||= begin - if params[:department_id] - d = Department.find(params[:department_id]) - @title = "Planung der Aufträge von #{d}" - d.orders.where('work_items.closed = ?', false).list - elsif params[:custom_list_id] - CustomList.where(item_type: Order.sti_name).find(params[:custom_list_id]).items.list - else - raise ActiveRecord::RecordNotFound - end - end + @orders ||= if params[:department_id] + d = Department.find(params[:department_id]) + @title = "Planung der Aufträge von #{d}" + d.orders.where(work_items: { closed: false }).list + elsif params[:custom_list_id] + CustomList.where(item_type: Order.sti_name).find(params[:custom_list_id]).items.list + else + raise ActiveRecord::RecordNotFound + end end def order @@ -48,8 +48,8 @@ def relevant_work_item_id def order_for_work_item_id(work_item_id) Order.joins('LEFT JOIN work_items ON ' \ - 'orders.work_item_id = ANY (work_items.path_ids)'). - find_by('work_items.id = ?', work_item_id) + 'orders.work_item_id = ANY (work_items.path_ids)') + .find_by('work_items.id = ?', work_item_id) end end end diff --git a/app/controllers/plannings/orders_controller.rb b/app/controllers/plannings/orders_controller.rb index d8d668f79..10ed15e0c 100644 --- a/app/controllers/plannings/orders_controller.rb +++ b/app/controllers/plannings/orders_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -5,12 +7,12 @@ module Plannings class OrdersController < BaseController - self.search_columns = %w(work_items.name work_items.shortname - work_items.path_names work_items.path_shortnames) + self.search_columns = %w[work_items.name work_items.shortname + work_items.path_names work_items.path_shortnames] skip_authorize_resource - before_action :load_possible_employees, only: [:new, :show] + before_action :load_possible_employees, only: %i[new show] private @@ -45,8 +47,8 @@ def params_with_restricted_items end def plannings_to_destroy - super.joins(:work_item). - where('? = ANY (work_items.path_ids)', order.work_item_id) + super.joins(:work_item) + .where('? = ANY (work_items.path_ids)', order.work_item_id) end end end diff --git a/app/controllers/portfolio_items_controller.rb b/app/controllers/portfolio_items_controller.rb index 726857d42..f439f16a3 100644 --- a/app/controllers/portfolio_items_controller.rb +++ b/app/controllers/portfolio_items_controller.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. class PortfolioItemsController < ManageController - self.permitted_attrs = [:name, :active] + self.permitted_attrs = %i[name active] end diff --git a/app/controllers/revenue_reports_controller.rb b/app/controllers/revenue_reports_controller.rb index 6f9c7c5bd..e8d7c2e5b 100644 --- a/app/controllers/revenue_reports_controller.rb +++ b/app/controllers/revenue_reports_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -35,13 +37,9 @@ def csv_filename name = 'revenue' period = @report&.period - if @report&.grouping_name - name += "_#{@report.grouping_name.underscore}" - end + name += "_#{@report.grouping_name.underscore}" if @report&.grouping_name - if period&.start_date - name += "_#{period.start_date.strftime('%Y-%m-%d')}" - end + name += "_#{period.start_date.strftime('%Y-%m-%d')}" if period&.start_date if period&.end_date && period&.end_date != period&.start_date @@ -52,15 +50,15 @@ def csv_filename end def report_type - grouping = params[:grouping].present? ? params[:grouping] : 'Department' + grouping = params[:grouping].presence || 'Department' REPORT_TYPES.find { |r| r.grouping_name == grouping } end def set_period super - if @period.nil? || @period.start_date.nil? || @period.end_date.nil? - @period = default_period - end + return unless @period.nil? || @period.start_date.nil? || @period.end_date.nil? + + @period = default_period end def default_period diff --git a/app/controllers/sectors_controller.rb b/app/controllers/sectors_controller.rb index c83063d25..c737530f5 100644 --- a/app/controllers/sectors_controller.rb +++ b/app/controllers/sectors_controller.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. class SectorsController < ManageController - self.permitted_attrs = [:name, :active] + self.permitted_attrs = %i[name active] end diff --git a/app/controllers/services_controller.rb b/app/controllers/services_controller.rb index 045994cac..d00e9e542 100644 --- a/app/controllers/services_controller.rb +++ b/app/controllers/services_controller.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. class ServicesController < ManageController - self.permitted_attrs = [:name, :active] + self.permitted_attrs = %i[name active] end diff --git a/app/controllers/status_controller.rb b/app/controllers/status_controller.rb index 5b5940103..dae3db824 100644 --- a/app/controllers/status_controller.rb +++ b/app/controllers/status_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -17,7 +19,7 @@ def readiness ready, status, message = assess_readiness http_code = ready ? :ok : :internal_server_error - render json: { status: status, message: message }, status: http_code + render json: { status:, message: }, status: http_code end private diff --git a/app/controllers/target_scopes_controller.rb b/app/controllers/target_scopes_controller.rb index 1b8939421..629e2aa58 100644 --- a/app/controllers/target_scopes_controller.rb +++ b/app/controllers/target_scopes_controller.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. class TargetScopesController < ManageController - self.permitted_attrs = [:name, :icon, :position, - :rating_green_description, :rating_orange_description, :rating_red_description] + self.permitted_attrs = %i[name icon position + rating_green_description rating_orange_description rating_red_description] end diff --git a/app/controllers/user_notifications_controller.rb b/app/controllers/user_notifications_controller.rb index d161bc52d..cb4367ee2 100644 --- a/app/controllers/user_notifications_controller.rb +++ b/app/controllers/user_notifications_controller.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. class UserNotificationsController < ManageController - self.permitted_attrs = [:date_from, :date_to, :message] + self.permitted_attrs = %i[date_from date_to message] end diff --git a/app/controllers/vacations_controller.rb b/app/controllers/vacations_controller.rb index d1a909a1b..b40f3693f 100644 --- a/app/controllers/vacations_controller.rb +++ b/app/controllers/vacations_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -8,7 +10,7 @@ class VacationsController < ApplicationController before_action :set_period def show - @graph = VacationGraph.new(@period) + @graph = Graphs::VacationGraph.new(@period) end private diff --git a/app/controllers/weekly_graph_controller.rb b/app/controllers/weekly_graph_controller.rb index 2714cecb0..c19153058 100644 --- a/app/controllers/weekly_graph_controller.rb +++ b/app/controllers/weekly_graph_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -8,7 +10,7 @@ class WeeklyGraphController < ApplicationController before_action :set_period def show - @graph = WorktimeGraph.new(@period || Period.past_month, employee) + @graph = Graphs::WorktimeGraph.new(@period || Period.past_month, employee) end private diff --git a/app/controllers/work_items_controller.rb b/app/controllers/work_items_controller.rb index f6b5a034a..08a100d04 100644 --- a/app/controllers/work_items_controller.rb +++ b/app/controllers/work_items_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -5,18 +7,18 @@ class WorkItemsController < ManageController self.permitted_attrs = :name, :shortname, :description, :parent_id - self.search_columns = [:path_shortnames, :path_names, :description] + self.search_columns = %i[path_shortnames path_names description] def search params[:q] ||= params[:term] respond_to do |format| format.json do - @work_items = WorkItem.recordable. - list. - where(search_conditions). - joins(:accounting_post). - includes(:accounting_post). - limit(20) + @work_items = WorkItem.recordable + .list + .where(search_conditions) + .joins(:accounting_post) + .includes(:accounting_post) + .limit(20) end end end diff --git a/app/controllers/working_conditions_controller.rb b/app/controllers/working_conditions_controller.rb index 1bac7d068..041701885 100644 --- a/app/controllers/working_conditions_controller.rb +++ b/app/controllers/working_conditions_controller.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. class WorkingConditionsController < ManageController - self.permitted_attrs = [:valid_from, :vacation_days_per_year, :must_hours_per_day] + self.permitted_attrs = %i[valid_from vacation_days_per_year must_hours_per_day] end diff --git a/app/controllers/workload_report_controller.rb b/app/controllers/workload_report_controller.rb index a25330f0b..eef320599 100644 --- a/app/controllers/workload_report_controller.rb +++ b/app/controllers/workload_report_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -5,9 +7,7 @@ class WorkloadReportController < ApplicationController include DryCrud::Rememberable - include WithPeriod - - self.remember_params = %w(start_date end_date department_id) + self.remember_params = %w[department_id] before_action :authorize_class @@ -34,6 +34,13 @@ def set_department @department = Department.where(id: params[:department_id]).first end + # rubocop:disable Naming/MemoizedInstanceVariableName + def set_period + super + @period ||= default_period + end + # rubocop:enable Naming/MemoizedInstanceVariableName + def default_period month = Time.zone.today.last_month Period.new(month.beginning_of_month, month.end_of_month) diff --git a/app/jobs/bi_export_job.rb b/app/controllers/workplaces_controller.rb similarity index 52% rename from app/jobs/bi_export_job.rb rename to app/controllers/workplaces_controller.rb index be660a1ae..708111935 100644 --- a/app/jobs/bi_export_job.rb +++ b/app/controllers/workplaces_controller.rb @@ -1,12 +1,10 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of +# frozen_string_literal: true + +# Copyright (c) 2006-2022, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class BIExportJob < CronJob - self.cron_expression = '0 1 * * *' - - def perform - BI::Export.new.run - end +class WorkplacesController < ManageController + self.permitted_attrs = [:name] end diff --git a/app/controllers/worktimes_controller.rb b/app/controllers/worktimes_controller.rb index 647ce2c0f..6a6eef3ef 100644 --- a/app/controllers/worktimes_controller.rb +++ b/app/controllers/worktimes_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -16,7 +18,7 @@ class WorktimesController < CrudController before_render_form :set_existing before_render_form :set_employees - FINISH = 'Abschliessen'.freeze + FINISH = 'Abschliessen' def index set_week_days @@ -30,22 +32,22 @@ def show def new super - if params[:template] - template = Worktime.find_by(id: params[:template]) - if template - @worktime.account_id = template.account_id - @worktime.ticket = template.ticket - @worktime.description = template.description - @worktime.billable = template.billable - @worktime.meal_compensation = template.meal_compensation - end - end + return unless params[:template] + + template = Worktime.find_by(id: params[:template]) + return unless template + + @worktime.account_id = template.account_id + @worktime.ticket = template.ticket + @worktime.description = template.description + @worktime.billable = template.billable + @worktime.meal_compensation = template.meal_compensation end def create(options = {}) if params[:redirect_to_self] work_date = params[:ordertime][:work_date] - options[:location] = new_ordertime_path(work_date: work_date) + options[:location] = new_ordertime_path(work_date:) end super(options) @@ -72,35 +74,35 @@ def create_default_worktime end def set_work_date - unless @worktime.work_date - @worktime.work_date = if params[:work_date] - params[:work_date] - elsif @period && @period.length == 1 - @period.start_date - else - Time.zone.today - end - end + return if @worktime.work_date + + @worktime.work_date = if params[:work_date] + params[:work_date] + elsif @period && @period.length == 1 + @period.start_date + else + Time.zone.today + end end def check_overlapping - if @worktime.report_type.is_a? StartStopType - conditions = ['NOT (work_item_id IS NULL AND absence_id IS NULL) AND ' \ - 'employee_id = :employee_id AND work_date = :work_date AND id <> :id AND (' \ - '(from_start_time <= :start_time AND to_end_time >= :end_time) OR ' \ - '(from_start_time >= :start_time AND from_start_time < :end_time) OR ' \ - '(to_end_time > :start_time AND to_end_time <= :end_time))', - { employee_id: @worktime.employee_id, - work_date: @worktime.work_date, - id: @worktime.id, - start_time: @worktime.from_start_time, - end_time: @worktime.to_end_time }] - overlaps = Worktime.where(conditions).includes(:work_item).to_a - if overlaps.present? - flash[:warning] = "#{@worktime}: Es besteht eine Überlappung mit mindestens einem anderen Eintrag:\n".html_safe - flash[:warning] += overlaps.collect { |o| ERB::Util.h(o) }.join("\n").html_safe - end - end + return unless @worktime.report_type.is_a? ReportType::StartStopType + + conditions = ['NOT (work_item_id IS NULL AND absence_id IS NULL) AND ' \ + 'employee_id = :employee_id AND work_date = :work_date AND id <> :id AND (' \ + '(from_start_time <= :start_time AND to_end_time >= :end_time) OR ' \ + '(from_start_time >= :start_time AND from_start_time < :end_time) OR ' \ + '(to_end_time > :start_time AND to_end_time <= :end_time))', + { employee_id: @worktime.employee_id, + work_date: @worktime.work_date, + id: @worktime.id, + start_time: @worktime.from_start_time, + end_time: @worktime.to_end_time }] + overlaps = Worktime.where(conditions).includes(:work_item).to_a + return if overlaps.blank? + + flash[:warning] = "#{@worktime}: Es besteht eine Überlappung mit mindestens einem anderen Eintrag:\n".html_safe + flash[:warning] += overlaps.collect { |o| ERB::Util.h(o) }.join("\n").html_safe end def check_employment @@ -111,17 +113,17 @@ def check_employment return end - if employment.percent.zero? - flash[:warning] = "Vorsicht, am #{l(@worktime.work_date)} wurde bereits unbezahlter Urlaub eingetragen".html_safe - return - end + return unless employment.percent.zero? + + flash[:warning] = "Vorsicht, am #{l(@worktime.work_date)} wurde bereits unbezahlter Urlaub eingetragen".html_safe + nil end def set_existing @work_date = @worktime.work_date - @existing = Worktime.where('employee_id = ? AND work_date = ?', @worktime.employee_id, @work_date). - order('type DESC, from_start_time, work_item_id'). - includes(:work_item, :absence) + @existing = Worktime.where('employee_id = ? AND work_date = ?', @worktime.employee_id, @work_date) + .order('type DESC, from_start_time, work_item_id') + .includes(:work_item, :absence) end def set_week_days @@ -184,11 +186,10 @@ def employee_id end # overwrite in subclass - def set_worktime_defaults - end + def set_worktime_defaults; end def record_other? - @user.management && (%w(1 true).include?(params[:other]) || other_employee_param?) + @user.management && (%w[1 true].include?(params[:other]) || other_employee_param?) end def other_employee_param? @@ -214,17 +215,17 @@ def build_entry end def assign_attributes - if params.key?(model_identifier) - # Set start/end time to nil, this way we correctly unset - # the time on "hours" change with entry.attributes = model_params - # Otherwise the start/end time recalculate the hours property. - params[model_identifier][:from_start_time] ||= nil - params[model_identifier][:to_end_time] ||= nil - - params[:other] = '1' if model_params[:employee_id] && @user.management - super - entry.employee = @user unless record_other? - end + return unless params.key?(model_identifier) + + # Set start/end time to nil, this way we correctly unset + # the time on "hours" change with entry.attributes = model_params + # Otherwise the start/end time recalculate the hours property. + params[model_identifier][:from_start_time] ||= nil + params[model_identifier][:to_end_time] ||= nil + + params[:other] = '1' if model_params[:employee_id] && @user.management + super + entry.employee = @user unless record_other? end def generic_evaluation @@ -236,10 +237,10 @@ def ivar_name(klass) end def check_has_accounting_post - unless entry.work_item.respond_to?(:accounting_post) - entry.errors.add(:work_item, 'Bitte wähle eine Buchungsposition aus') - throw :abort - end + return if entry.work_item.respond_to?(:accounting_post) + + entry.errors.add(:work_item, 'Bitte wähle eine Buchungsposition aus') + throw :abort end def check_worktimes_committed diff --git a/app/domain/api_client.rb b/app/domain/api_client.rb index dc31be1be..e3f43b2e3 100644 --- a/app/domain/api_client.rb +++ b/app/domain/api_client.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + class ApiClient def authenticate(user, password) - return self if Settings.api_client.user == user && Settings.api_client.password == password + self if Settings.api_client.user == user && Settings.api_client.password == password end def id diff --git a/app/domain/apidocs/helper.rb b/app/domain/apidocs/helper.rb index d9dd8de88..86a0968ff 100644 --- a/app/domain/apidocs/helper.rb +++ b/app/domain/apidocs/helper.rb @@ -32,7 +32,7 @@ def nested_root_path end def nested_controller_id - controller_class.model_class.model_name.route_key.singularize + '_id' + "#{controller_class.model_class.model_name.route_key.singularize}_id" end def nested_model_name @@ -40,11 +40,11 @@ def nested_model_name end def available_includes(controller = controller_class) - controller. - serializer&. - relationships_to_serialize&. - keys&. - sort + controller + .serializer + &.relationships_to_serialize + &.keys + &.sort end def include_description(controller = controller_class) @@ -66,7 +66,7 @@ def path_spec(swagger_doc, helper, type) helper.setup_tags(self) helper.parameters(self, helper, type) response 200 do - key :description, summary + ' Response' + key :description, "#{summary} Response" helper.response_schema(self, helper, type) end end @@ -80,14 +80,14 @@ def setup_tags(swagger_doc) end def parameters(swagger_doc, helper, type) - parameter_id(swagger_doc, helper) if [:show, :nested].include?(type.to_sym) + parameter_id(swagger_doc, helper) if %i[show nested].include?(type.to_sym) parameter_custom(swagger_doc, type) clazz = type.to_sym == :nested ? helper.nested_class : controller_class - if available_includes(clazz).present? - desc = include_description(clazz) - parameter_include(swagger_doc, desc) - end + return if available_includes(clazz).blank? + + desc = include_description(clazz) + parameter_include(swagger_doc, desc) end def parameter_id(swagger_doc, helper) @@ -132,7 +132,7 @@ def response_schema(swagger_doc, helper, type) swagger_doc.schema do key :type, :array items do - key :'$ref', ref + key :$ref, ref end end end diff --git a/app/domain/apidocs/nested_controller_setup.rb b/app/domain/apidocs/nested_controller_setup.rb index e797cf2c6..7b2d2c070 100644 --- a/app/domain/apidocs/nested_controller_setup.rb +++ b/app/domain/apidocs/nested_controller_setup.rb @@ -49,7 +49,7 @@ def nested?(controller) nested = [] # nested << controller.optional_nesting || [] - nested << controller.nesting || [] + (nested << controller.nesting) || [] nested.flatten.include? controller_class.model_class end diff --git a/app/domain/apidocs/setup.rb b/app/domain/apidocs/setup.rb index 37a6b6876..695bbd65d 100644 --- a/app/domain/apidocs/setup.rb +++ b/app/domain/apidocs/setup.rb @@ -73,9 +73,9 @@ def setup_models def setup_model(serializer_class) swagger_spec.instance_exec(self) do |helper| - model_name = serializer_class.name.demodulize.gsub(/Serializer\z/, '').to_sym + model_name = serializer_class.name.demodulize.delete_suffix('Serializer').to_sym swagger_schema model_name do - serializer_class.attributes_to_serialize.keys.each do |attr| + serializer_class.attributes_to_serialize.each_key do |attr| annotation = serializer_class.attribute_annotations[attr] property attr do instance_exec(annotation, self, &helper.method(:setup_property)) @@ -102,15 +102,14 @@ def setup_controller(controller_class) def collect_serializers(*serializers, collected_serializers: []) collected_serializers += serializers - serializers. - map(&:relationships_to_serialize). - compact. - flat_map(&:values). - map { |relationship| relationship.serializer.to_s.constantize }. - each do |related_serializer| + serializers + .filter_map(&:relationships_to_serialize) + .flat_map(&:values) + .map { |relationship| relationship.serializer.to_s.constantize } + .each do |related_serializer| next if collected_serializers.include?(related_serializer) - collect_serializers(related_serializer, collected_serializers: collected_serializers) + collect_serializers(related_serializer, collected_serializers:) end collected_serializers diff --git a/app/domain/apidocs/tags_setup.rb b/app/domain/apidocs/tags_setup.rb index 59a14633c..f1ea5d7ae 100644 --- a/app/domain/apidocs/tags_setup.rb +++ b/app/domain/apidocs/tags_setup.rb @@ -19,7 +19,7 @@ def get_tag_by_path(path) next if tag['include'].blank? tag['include'].each do |inc| - return tag['name'] if path =~ /#{inc}/i + return tag['name'] if /#{inc}/i.match?(path) end end end @@ -38,7 +38,7 @@ def tags def load_tags require 'yaml' - YAML.load_file(Rails.root.join('config', 'swagger-tags.yml')) # TODO: what is this for? fill yml with sensible values + YAML.load_file(Rails.root.join('config/swagger-tags.yml')) # TODO: what is this for? fill yml with sensible values end end end diff --git a/app/domain/bi.rb b/app/domain/bi.rb deleted file mode 100644 index c558369f3..000000000 --- a/app/domain/bi.rb +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -module BI - class ConfigurationError < StandardError; end - - def self.init - if Settings.influxdb&.export - check_config! - - BIExportJob.schedule if Delayed::Job.table_exists? - end - end - - private - - def self.check_config! - settings = Settings.influxdb - - missing = %i[host port org token use_ssl].select do |setting| - settings.send(setting).nil? - end - - return if missing.empty? - - raise ConfigurationError, "Settings influxdb.{#{missing.join(', ')}} missing" - end -end diff --git a/app/domain/bi/export.rb b/app/domain/bi/export.rb deleted file mode 100644 index 643baedb8..000000000 --- a/app/domain/bi/export.rb +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -module BI - class Export - def run - stats = [ - [Reports::Revenue::BI.new.stats, 'Revenue'], - [Reports::BIWorkload.new.stats, 'Workload'], - [Order::Report::BI.new.stats, 'Orders'], - [role_distribution, 'RoleDistribution'] - ] - stats << [Crm::HighriseStats.new.stats, 'Highrise'] if highrise_enabled? - - export(stats) - end - - private - - def highrise_enabled? - Crm.instance.is_a?(Crm::Highrise) - end - - def role_distribution - # RoleDistributionReport ... - [] - end - - def export(stats) - exporter = InfluxExporter.new(Settings.influxdb) - - exporter.ensure_buckets(stats.map { |_s, bucket| bucket }) - stats.each { |data, bucket| exporter.export(data, bucket) } - end - end -end diff --git a/app/domain/bi/influx_ensure_bucket.rb b/app/domain/bi/influx_ensure_bucket.rb deleted file mode 100644 index fa5e55b88..000000000 --- a/app/domain/bi/influx_ensure_bucket.rb +++ /dev/null @@ -1,39 +0,0 @@ -module BI - class InfluxEnsureBucket - class OrgNotFound < StandardError; end - - def initialize(client, org) - @client = client - @api = InfluxDB2::API::Client.new(client) - @org = find_org(org, @api) - end - - def bucket(bucket) - return if bucket_exists?(bucket, @org, @api) - - create_bucket(bucket, @org, @api) - end - - private - - def find_org(name, api) - org = - api.create_organizations_api.get_orgs.orgs.select do |it| - it.name == name - end.first - - raise OrgNotFound, "No org with name #{name} present" if org.nil? - org - end - - def bucket_exists?(name, org, api) - api.create_buckets_api.get_buckets(name: name).buckets.any? - end - - def create_bucket(name, org, api) - request = - InfluxDB2::API::PostBucketRequest.new(org_id: org.id, name: name) - api.create_buckets_api.post_buckets(request) - end - end -end diff --git a/app/domain/bi/influx_exporter.rb b/app/domain/bi/influx_exporter.rb deleted file mode 100644 index 71883870c..000000000 --- a/app/domain/bi/influx_exporter.rb +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -module BI - class InfluxExporter - # @param settings `Settings.influxdb` - def initialize(settings) - @client = make_client(settings) - @org = settings.org - @use_ssl = settings.use_ssl - end - - def ensure_buckets(buckets) - ensurer = InfluxEnsureBucket.new(@client, @org) - buckets.each { |bucket| ensurer.bucket(bucket) } - end - - def export(data, bucket) - return if data.empty? - - sanitize_fields!(data) - - api = @client.create_write_api - api.write(data: data, bucket: bucket) - end - - private - - def sanitize_fields!(data) - # If we write a field with type int first and then try to write a floating point - # value afterwards, we get - # `failure writing points to database: partial write: field type conflict [...]` - data.each do |e| - e[:fields].transform_values! { |v| v.is_a?(Numeric) ? v.to_f : v } - end - end - - def make_client(settings) - scheme = @use_ssl ? 'https' : 'http' - url = "#{scheme}://#{settings.host}:#{settings.port}" - InfluxDB2::Client.new( - url, - settings.token, - org: settings.org, - precision: InfluxDB2::WritePrecision::SECOND, - bucket: 'PtimeDefault', - use_ssl: settings.use_ssl - ) - end - end -end diff --git a/app/domain/crm.rb b/app/domain/crm.rb index 805de63fa..9589811fb 100644 --- a/app/domain/crm.rb +++ b/app/domain/crm.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -8,9 +10,9 @@ module Crm cattr_accessor :instance def self.init - if Settings.highrise.api_token - Crm.instance = Crm::Highrise.new - CrmSyncJob.schedule if Delayed::Job.table_exists? - end + return unless Settings.highrise.api_token + + Crm.instance = Crm::Highrise.new + CrmSyncJob.schedule if Delayed::Job.table_exists? end end diff --git a/app/domain/crm/base.rb b/app/domain/crm/base.rb index 9f9a58e11..be45666cc 100644 --- a/app/domain/crm/base.rb +++ b/app/domain/crm/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -21,24 +23,19 @@ def name end # CRM icon to display for links - def icon - end + def icon; end # CRM Url for the given client - def client_url(_client) - end + def client_url(_client); end # CRM Url for the given contact - def contact_url(_contact) - end + def contact_url(_contact); end # CRM Url for the given order - def order_url(_order) - end + def order_url(_order); end # Find an order with the given key in the crm - def find_order(_key) - end + def find_order(_key); end # Find all contacts for a given client in the crm def find_client_contacts(_client) @@ -46,20 +43,16 @@ def find_client_contacts(_client) end # Find a contact with the given key in the crm - def find_person(_key) - end + def find_person(_key); end # Find people with the given email - def find_people_by_email(_email) - end + def find_people_by_email(_email); end # Sync all entities from the crm, discarding local changes. - def sync_all - end + def sync_all; end # Sync a single AdditionalCrmOrder - def sync_additional_order(additional) - end + def sync_additional_order(additional); end # Whether only orders from the CRM are allowed or also local ones. def restrict_local? diff --git a/app/domain/crm/error.rb b/app/domain/crm/error.rb index fff9d1722..2d28b6921 100644 --- a/app/domain/crm/error.rb +++ b/app/domain/crm/error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/domain/crm/highrise.rb b/app/domain/crm/highrise.rb index 47fdc741b..9b4fe8018 100644 --- a/app/domain/crm/highrise.rb +++ b/app/domain/crm/highrise.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -38,10 +40,10 @@ def find_order(key) end def verify_deal_party_type(deal) - unless deal.party.type.casecmp('company').zero? - fail Crm::Error, I18n.t('error.crm.highrise.order_not_on_company', - party_type: deal.party.type) - end + return if deal.party.type.casecmp('company').zero? + + raise Crm::Error, I18n.t('error.crm.highrise.order_not_on_company', + party_type: deal.party.type) end def find_client_contacts(client) @@ -55,7 +57,7 @@ def find_person(key) end def find_people_by_email(email) - ::Highrise::Person.search(email: email) + ::Highrise::Person.search(email:) end def sync_all @@ -147,23 +149,19 @@ def contact_attributes(person) end def sync_crm_entities(entities) - entities.where('crm_key IS NOT NULL').find_each do |entity| - begin - yield entity - rescue ActiveResource::ResourceNotFound - entity.update_attribute(:crm_key, nil) - rescue ActiveRecord::RecordInvalid => error - notify_sync_error(error, entity, error.record) - rescue => error - notify_sync_error(error, entity) - end + entities.where.not(crm_key: nil).find_each do |entity| + yield entity + rescue ActiveResource::ResourceNotFound + entity.update_attribute(:crm_key, nil) + rescue ActiveRecord::RecordInvalid => e + notify_sync_error(e, entity, e.record) + rescue StandardError => e + notify_sync_error(e, entity) end end def crm_entity_url(model, entity) - if entity.respond_to?(:crm_key) - entity = entity.crm_key - end + entity = entity.crm_key if entity.respond_to?(:crm_key) "#{base_url}/#{model}/#{entity}" if entity.present? end @@ -189,10 +187,10 @@ def sentry? def record_to_params(record, prefix = 'record') { - "#{prefix}_type" => record.class.name, - "#{prefix}_id" => record.id, - "#{prefix}_label" => record.try(:label) || record.to_s, - "#{prefix}_errors" => record.errors.messages, + "#{prefix}_type" => record.class.name, + "#{prefix}_id" => record.id, + "#{prefix}_label" => record.try(:label) || record.to_s, + "#{prefix}_errors" => record.errors.messages, "#{prefix}_changes" => record.changes } end diff --git a/app/domain/crm/highrise_stats.rb b/app/domain/crm/highrise_stats.rb deleted file mode 100644 index 9704bcd3a..000000000 --- a/app/domain/crm/highrise_stats.rb +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -module Crm - class HighriseStats < Base - NO_CATEGORY = '(no category)'.freeze - - def stats - # Job runs at 1 in the morning, collect data from yesterday - month_from = 1.day.ago.beginning_of_month.utc - month_deals = fetch_deals(modified_since: month_from) - pending_deals = fetch_deals(status: 'pending') - - month_stats(month_deals, month_from) + stale_stats(pending_deals) - end - - private - - def fetch_deals(modified_since: nil, status: nil) - # This ignores paging. If we ever get more than 500 deals we'll have to do multiple requests. - - params = {} - params[:since] = timestamp(modified_since) unless modified_since.nil? - params[:status] = status unless status.nil? - - ::Highrise::Deal.find(:all, params: params).yield_self do |deals| - modified_since.nil? ? deals : status_changed(deals, modified_since) - end - end - - def group(deals) - deals.group_by(&:status).transform_values do |deals| - deals.group_by { |deal| deal.try(:category).try(:name) || NO_CATEGORY } - end - end - - def timestamp(time) - # Highrise wants 'yyyymmddhhmmss' - # see https://github.com/basecamp/highrise-api/blob/master/sections/deals.md - time.strftime('%Y%m%d000000') - end - - def month_stats(deals, month) - build_stats( - group(deals).except('pending'), - month: month.strftime('%Y-%m') - ) - end - - def stale_stats(deals) - deals.partition { |deal| deal.updated_at <= 3.months.ago.utc }.zip( - [true, false] - ) - .flat_map { |deals, stale| build_stats(group(deals), stale: stale) } - end - - def build_stats(deals, tags = {}) - count_stats(deals, tags) + volume_stats(deals, tags) - end - - def count_stats(grouped_deals, tags) - map_deals(grouped_deals) do |status, category, deals| - { - name: 'highrise_deals', - fields: { count: deals.count }, - tags: { category: category, status: status }.merge(tags) - } - end - end - - def volume_stats(grouped_deals, tags) - map_deals(grouped_deals) do |status, category, deals| - value = deals.map { |deal| volume(deal) }.sum - - { - name: 'highrise_volume', - fields: { value: value }, - tags: { category: category, status: status }.merge(tags) - } - end - end - - def status_changed(deals, since) - deals.select do |deal| - next deal.created_at >= since if deal.status_changed_on.nil? - deal.status_changed_on >= since - end - end - - def map_deals(grouped_deals) - grouped_deals.flat_map do |status, deals_by_category| - deals_by_category.map do |category, deals| - yield(status, category, deals) - end - end.compact - end - - def volume(deal) - return 0 if deal.price.nil? - deal.price_type == 'fixed' ? deal.price : deal.price * deal.duration - end - end -end diff --git a/app/domain/evaluations/absences_eval.rb b/app/domain/evaluations/absences_eval.rb index 691152cad..b373e57f3 100644 --- a/app/domain/evaluations/absences_eval.rb +++ b/app/domain/evaluations/absences_eval.rb @@ -1,32 +1,34 @@ -# -*- coding: utf-8 -*- +# frozen_string_literal: true # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class AbsencesEval < Evaluation - self.sub_evaluation = 'employeeabsences' - self.division_column = :employee_id - self.label = 'Members Absenzen' - self.absences = true - self.detail_columns = detail_columns.reject { |i| i == :billable } - self.detail_labels = detail_labels.merge(account: 'Absenz') +module Evaluations + class AbsencesEval < Evaluations::Evaluation + self.sub_evaluation = 'employeeabsences' + self.division_column = :employee_id + self.label = 'Members Absenzen' + self.absences = true + self.detail_columns = detail_columns.reject { |i| i == :billable } + self.detail_labels = detail_labels.merge(account: 'Absenz') - def initialize - super(Employee) - end + def initialize(**search_conditions) + super(Employee, **search_conditions) + end - def divisions(period = nil) - Employee.employed_ones(period || Period.current_year) - end + def divisions(period = nil) + Employee.employed_ones(period || Period.current_year) + end - def employee_id - division.id if division - end + def employee_id + division&.id + end - def division_supplement(_user) - [[:remaining_vacations, 'Übrige Ferien', 'right'], - [:overtime_vacations_tooltip, '', 'left']] + def division_supplement(_user) + [[:remaining_vacations, 'Übrige Ferien', 'right'], + [:overtime_vacations_tooltip, '', 'left']] + end end end diff --git a/app/domain/evaluations/client_work_items_eval.rb b/app/domain/evaluations/client_work_items_eval.rb index 23286c1c5..55147f417 100644 --- a/app/domain/evaluations/client_work_items_eval.rb +++ b/app/domain/evaluations/client_work_items_eval.rb @@ -1,7 +1,11 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class ClientWorkItemsEval < SubWorkItemsEval +module Evaluations + class ClientWorkItemsEval < Evaluations::SubWorkItemsEval + end end diff --git a/app/domain/evaluations/clients_eval.rb b/app/domain/evaluations/clients_eval.rb index 85789020b..2f4e30baa 100644 --- a/app/domain/evaluations/clients_eval.rb +++ b/app/domain/evaluations/clients_eval.rb @@ -1,28 +1,32 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class ClientsEval < Evaluation - self.division_column = 'work_items.path_ids[1]' - self.division_join = :work_item - self.sub_evaluation = 'clientworkitems' - self.label = 'Kunden' - self.total_details = false - self.billable_hours = true - self.planned_hours = true +module Evaluations + class ClientsEval < Evaluations::Evaluation + self.division_column = Arel.sql('work_items.path_ids[1]') + self.division_join = :work_item + self.sub_evaluation = 'clientworkitems' + self.label = 'Kunden' + self.total_details = false + self.billable_hours = true + self.planned_hours = true - def initialize - super(Client) - end + def initialize + super(Client) + end - def divisions(_period = nil) - WorkItem.joins(:client).list - end + def divisions(_period = nil) + WorkItem.joins(:client).list + end - def set_division_id(division_id = nil) - return if division_id.nil? + def set_division_id(division_id = nil) + return if division_id.nil? - @division = WorkItem.find(division_id.to_i) + @division = WorkItem.find(division_id.to_i) + end end end diff --git a/app/domain/evaluations/department_orders_eval.rb b/app/domain/evaluations/department_orders_eval.rb index 13b63d6af..fb914a94e 100644 --- a/app/domain/evaluations/department_orders_eval.rb +++ b/app/domain/evaluations/department_orders_eval.rb @@ -1,28 +1,32 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class DepartmentOrdersEval < WorkItemsEval - self.division_join = nil - self.division_column = 'orders.work_item_id' - self.billable_hours = true - self.planned_hours = true +module Evaluations + class DepartmentOrdersEval < Evaluations::WorkItemsEval + self.division_join = nil + self.division_column = 'orders.work_item_id' + self.billable_hours = true + self.planned_hours = true - def initialize(department_id) - super(Department.find(department_id)) - end + def initialize(department_id) + super(Department.find(department_id)) + end - def divisions(_period = nil) - WorkItem.joins(:order).includes(:order).where(orders: { department_id: category.id }).list - end + def divisions(_period = nil) + WorkItem.joins(:order).includes(:order).where(orders: { department_id: category.id }).list + end - def division_supplement(_user) - [[:order_completed, 'Abschluss erledigt', 'left'], - [:order_committed, 'Abschluss freigegeben', 'left']] - end + def division_supplement(_user) + [[:order_completed, 'Abschluss erledigt', 'left'], + [:order_committed, 'Abschluss freigegeben', 'left']] + end - def include_no_period_zero_totals - false + def include_no_period_zero_totals + false + end end end diff --git a/app/domain/evaluations/departments_eval.rb b/app/domain/evaluations/departments_eval.rb index d8b06d821..98279372b 100644 --- a/app/domain/evaluations/departments_eval.rb +++ b/app/domain/evaluations/departments_eval.rb @@ -1,20 +1,24 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class DepartmentsEval < Evaluation - self.division_column = 'orders.department_id' - self.division_join = 'INNER JOIN work_items ON work_items.id = worktimes.work_item_id ' \ - 'INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)' - self.division_planning_join = 'INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)' - self.sub_evaluation = 'departmentorders' - self.label = 'Organisationseinheiten' - self.total_details = false - self.billable_hours = true - self.planned_hours = true +module Evaluations + class DepartmentsEval < Evaluations::Evaluation + self.division_column = 'orders.department_id' + self.division_join = 'INNER JOIN work_items ON work_items.id = worktimes.work_item_id ' \ + 'INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)' + self.division_planning_join = 'INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)' + self.sub_evaluation = 'departmentorders' + self.label = 'Organisationseinheiten' + self.total_details = false + self.billable_hours = true + self.planned_hours = true - def initialize - super(Department) + def initialize + super(Department) + end end end diff --git a/app/domain/evaluations/employee_absences_eval.rb b/app/domain/evaluations/employee_absences_eval.rb index 52ed2e04c..8528c38d2 100644 --- a/app/domain/evaluations/employee_absences_eval.rb +++ b/app/domain/evaluations/employee_absences_eval.rb @@ -1,30 +1,34 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class EmployeeAbsencesEval < Evaluation - self.division_method = :absences - self.division_column = :absence_id - self.label = 'Absenzen' - self.absences = true - self.category_ref = :employee_id - self.detail_columns = detail_columns.reject { |i| i == :billable } - self.detail_labels = detail_labels.merge(account: 'Absenz') +module Evaluations + class EmployeeAbsencesEval < Evaluations::Evaluation + self.division_method = :absences + self.division_column = :absence_id + self.label = 'Absenzen' + self.absences = true + self.category_ref = :employee_id + self.detail_columns = detail_columns.reject { |i| i == :billable } + self.detail_labels = detail_labels.merge(account: 'Absenz') - def initialize(employee_id) - super(Employee.find(employee_id)) - end + def initialize(employee_id, **search_conditions) + super(Employee.find(employee_id), **search_conditions) + end - def for?(user) - category == user - end + def for?(user) + category == user + end - def employee_id - category.id - end + def employee_id + category.id + end - def account_id - division.id if division + def account_id + division&.id + end end end diff --git a/app/domain/evaluations/employee_sub_work_items_eval.rb b/app/domain/evaluations/employee_sub_work_items_eval.rb index 6dbf3d612..1fa591003 100644 --- a/app/domain/evaluations/employee_sub_work_items_eval.rb +++ b/app/domain/evaluations/employee_sub_work_items_eval.rb @@ -1,33 +1,37 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class EmployeeSubWorkItemsEval < SubWorkItemsEval - include Conditioner +module Evaluations + class EmployeeSubWorkItemsEval < Evaluations::SubWorkItemsEval + include Conditioner - self.sub_work_items_eval = 'employeesubworkitems' - self.sub_evaluation = nil - self.detail_columns = detail_columns.collect { |i| i == :hours ? :times : i } - self.billable_hours = false - self.planned_hours = false + self.sub_work_items_eval = 'employeesubworkitems' + self.sub_evaluation = nil + self.detail_columns = detail_columns.collect { |i| i == :hours ? :times : i } + self.billable_hours = false + self.planned_hours = false - attr_reader :employee_id + attr_reader :employee_id - def initialize(work_item_id, employee_id) - super(work_item_id) - @employee_id = employee_id - end + def initialize(work_item_id, employee_id) + super(work_item_id) + @employee_id = employee_id + end - def for?(user) - employee_id == user.id - end + def for?(user) + employee_id == user.id + end - def worktime_query(receiver, period = nil, division = nil) - super(receiver, period, division).where(employee_id: employee_id) - end + def worktime_query(receiver, period = nil, division = nil) + super(receiver, period, division).where(employee_id:) + end - def sub_work_items_evaluation(division = nil) - sub_work_items_eval + employee_id.to_s if division.children? + def sub_work_items_evaluation(division = nil) + sub_work_items_eval + employee_id.to_s if division.children? + end end end diff --git a/app/domain/evaluations/employee_work_items_eval.rb b/app/domain/evaluations/employee_work_items_eval.rb index 5463bafe5..f87ce4b5c 100644 --- a/app/domain/evaluations/employee_work_items_eval.rb +++ b/app/domain/evaluations/employee_work_items_eval.rb @@ -1,38 +1,42 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class EmployeeWorkItemsEval < WorkItemsEval - self.category_ref = :employee_id - self.sub_evaluation = nil - self.division_method = :alltime_main_work_items - self.sub_work_items_eval = 'employeesubworkitems' - self.detail_columns = detail_columns.collect { |i| i == :hours ? :times : i } +module Evaluations + class EmployeeWorkItemsEval < Evaluations::WorkItemsEval + self.category_ref = :employee_id + self.sub_evaluation = nil + self.division_method = :alltime_main_work_items + self.sub_work_items_eval = 'employeesubworkitems' + self.detail_columns = detail_columns.collect { |i| i == :hours ? :times : i } - def initialize(employee_id) - super(Employee.find(employee_id)) - end + def initialize(employee_id) + super(Employee.find(employee_id)) + end - def for?(user) - category == user - end + def for?(user) + category == user + end - def division_supplement(_user) - [] - end + def division_supplement(_user) + [] + end - def employee_id - category.id - end + def employee_id + category.id + end - def sub_work_items_evaluation(work_item = nil) - sub_work_items_eval + employee_id.to_s if work_item && work_item.children? - end + def sub_work_items_evaluation(work_item = nil) + sub_work_items_eval + employee_id.to_s if work_item&.children? + end - def set_division_id(division_id = nil) - return if division_id.nil? + def set_division_id(division_id = nil) + return if division_id.nil? - @division = WorkItem.find(division_id.to_i) + @division = WorkItem.find(division_id.to_i) + end end end diff --git a/app/domain/evaluations/employees_eval.rb b/app/domain/evaluations/employees_eval.rb index 1d2b2c94e..478da8237 100644 --- a/app/domain/evaluations/employees_eval.rb +++ b/app/domain/evaluations/employees_eval.rb @@ -1,53 +1,55 @@ -# -*- coding: utf-8 -*- +# frozen_string_literal: true # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class EmployeesEval < Evaluation - self.division_column = 'employee_id' - self.sub_evaluation = 'employeeworkitems' - self.label = 'Member Zeit' - self.total_details = false +module Evaluations + class EmployeesEval < Evaluations::Evaluation + self.division_column = 'employee_id' + self.sub_evaluation = 'employeeworkitems' + self.label = 'Member Zeit' + self.total_details = false - def initialize(department_id = 0) - @department_id = department_id.to_i - super(Employee) - end + def initialize(department_id = 0) + @department_id = department_id.to_i + super(Employee) + end - def divisions(period = nil) - employees = if period - Employee.list - else - Employee.employed_ones(Period.current_year) - end - - if @department_id != 0 - employees.where(department_id: @department_id) - else - employees + def divisions(period = nil) + employees = if period + Employee.list + else + Employee.employed_ones(Period.current_year) + end + + if @department_id.zero? + employees + else + employees.where(department_id: @department_id) + end end - end - def employee_id - division.id if division - end + def employee_id + division&.id + end - def sum_total_times(period = nil) - query = if @department_id != 0 - Department.find(@department_id).employee_worktimes - else - Worktime.all - end - query = query.where(type: worktime_type).in_period(period) - query_time_sums(query) - end + def sum_total_times(period = nil) + query = if @department_id.zero? + Worktime.all + else + Department.find(@department_id).employee_worktimes + end + query = query.where(type: worktime_type).in_period(period) + query_time_sums(query) + end - def division_supplement(_user) - [[:overtime, 'Überstunden', 'right'], - [:overtime_vacations_tooltip, '', 'left'], - [:worktime_commits, 'Freigabe', 'left'], - [:worktime_reviews, 'Kontrolle', 'left']] + def division_supplement(_user) + [[:overtime, 'Überstunden', 'right'], + [:overtime_vacations_tooltip, '', 'left'], + [:worktime_commits, 'Freigabe', 'left'], + [:worktime_reviews, 'Kontrolle', 'left']] + end end end diff --git a/app/domain/evaluations/evaluation.rb b/app/domain/evaluations/evaluation.rb index 442d26eae..e376a871e 100644 --- a/app/domain/evaluations/evaluation.rb +++ b/app/domain/evaluations/evaluation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -9,301 +11,306 @@ # The worktime information may be constrained to certain periods of time. # # This class is abstract, subclasses generally override the class constants for customization. -class Evaluation - class_attribute :division_method, :division_column, :division_join, :division_planning_join, - :sub_evaluation, :sub_work_items_eval, :label, :absences, - :total_details, :billable_hours, :planned_hours, :category_ref, :detail_columns, - :detail_labels +module Evaluations + class Evaluation + class_attribute :division_method, :division_column, :division_join, :division_planning_join, + :sub_evaluation, :sub_work_items_eval, :label, :absences, + :total_details, :billable_hours, :planned_hours, :category_ref, :detail_columns, + :detail_labels - # The method to send to the category object to retrieve a list of divisions. - self.division_method = :list + # The method to send to the category object to retrieve a list of divisions. + self.division_method = :list - # Next lower evaluation for divisions, which will be acting as the category there. - self.sub_evaluation = nil + # Next lower evaluation for divisions, which will be acting as the category there. + self.sub_evaluation = nil - self.sub_work_items_eval = nil + self.sub_work_items_eval = nil - # Name of the evaluation to be displayed - self.label = '' + # Name of the evaluation to be displayed + self.label = '' - # Whether this Evaluation is for absences or order times. - self.absences = false + # Whether this Evaluation is for absences or order times. + self.absences = false - # Whether details for totals are possible. - self.total_details = true + # Whether details for totals are possible. + self.total_details = true - # Whether to show billing hours beside performed hours - self.billable_hours = false + # Whether to show billing hours beside performed hours + self.billable_hours = false - # Whether to show planned hours and difference in separate columns - self.planned_hours = false + # Whether to show planned hours and difference in separate columns + self.planned_hours = false - # The field of a division referencing the category entry in the database. - # May be nil if not required for this Evaluation (default). - self.category_ref = nil + # The field of a division referencing the category entry in the database. + # May be nil if not required for this Evaluation (default). + self.category_ref = nil - # Columns to display in the detail view - self.detail_columns = [:work_date, :hours, :employee, :account, :billable, - :ticket, :description] + # Columns to display in the detail view + self.detail_columns = %i[work_date hours employee account billable + ticket description] - # Table captions for detail columns - self.detail_labels = { work_date: 'Datum', - hours: 'Stunden', - times: 'Zeiten', - employee: 'Wer', - account: 'Projekt', - billable: '$', - ticket: 'Ticket', - description: 'Bemerkungen' } + # Table captions for detail columns + self.detail_labels = { work_date: 'Datum', + hours: 'Stunden', + times: 'Zeiten', + employee: 'Wer', + account: 'Projekt', + billable: '$', + ticket: 'Ticket', + description: 'Bemerkungen' } - attr_reader :category, # category - :division # selected division for detail Evaluations, nil otherwise + attr_reader :category, # category + :division, # selected division for detail Evaluations, nil otherwise + :search_conditions - ############### Time Evaluation Functions ############### + ############### Time Evaluation Functions ############### - # Returns a list of all division objects for the represented category. - # May be parameterized by a period. This is ignored by default. - def divisions(_period = nil) - category.send(division_method).list - end + # Returns a list of all division objects for the represented category. + # May be parameterized by a period. This is ignored by default. + def divisions(_period = nil) + category.send(division_method).list + end - # The record identifier of the category, 0 if category is not an active record - def category_id - category.is_a?(Class) ? 0 : category.id - end + # The record identifier of the category, 0 if category is not an active record + def category_id + category.is_a?(Class) ? 0 : category.id + end - def sum_times_grouped(period) - query = worktime_query(category, period). - joins(division_join). - group(division_column) - query_time_sums(query, division_column) - end + def sum_times_grouped(period) + query = worktime_query(category, period) + .joins(division_join) + .group(division_column) + query_time_sums(query, division_column) + end - # Sums all worktimes for a given period. - # If a division is passed or set previously, their sum will be returned. - # Otherwise the sum of all worktimes in the main category is returned. - def sum_times(period, div = nil, scope = nil) - query = worktime_query(div || division || category, - period, - div || division) - query = query.merge(scope) if scope - query.sum(:hours).to_f - end + # Sums all worktimes for a given period. + # If a division is passed or set previously, their sum will be returned. + # Otherwise the sum of all worktimes in the main category is returned. + def sum_times(period, div = nil, scope = nil) + query = worktime_query(div || division || category, + period, + div || division) + query = query.merge(scope) if scope + query.sum(:hours).to_f + end - # Sums all worktimes for the category in a given period. - def sum_total_times(period = nil) - query = worktime_query(category, period) - query_time_sums(query) - end + # Sums all worktimes for the category in a given period. + def sum_total_times(period = nil) + query = worktime_query(category, period) + query_time_sums(query) + end - # Returns a list of all Worktime entries for this Evaluation in the given period - # of time. - def times(period) - worktime_query(division || category, period, division). - order('worktimes.work_date ASC, worktimes.from_start_time, ' \ - 'worktimes.work_item_id, worktimes.employee_id') - end + # Returns a list of all Worktime entries for this Evaluation in the given period + # of time. + def times(period) + worktime_query(division || category, period, division) + .order('worktimes.work_date ASC, worktimes.from_start_time, ' \ + 'worktimes.work_item_id, worktimes.employee_id') + end - def worktime_query(receiver, period = nil, division = nil) - query = receiver.worktimes.where(type: worktime_type).in_period(period) - query = query.where("? = #{category_ref}", category_id) if division && category_ref - query - end + def worktime_query(receiver, period = nil, division = nil) + query = receiver.worktimes.where(type: worktime_type).in_period(period) + query = query.where("? = #{category_ref}", category_id) if division && category_ref + query = query.where(search_conditions) if search_conditions.present? + query + end - ############### Planning Evaluation Functions ############### + ############### Planning Evaluation Functions ############### - def sum_plannings_grouped(period) - query = planning_query(category).joins(division_planning_join).group(division_column) - if division_column - query_grouped_planning_sums(query, period, division_column) - else - query_planning_sums(query, period) + def sum_plannings_grouped(period) + query = planning_query(category).joins(division_planning_join).group(division_column) + if division_column + query_grouped_planning_sums(query, period, division_column) + else + query_planning_sums(query, period) + end end - end - def sum_total_plannings(period = nil) - query_planning_sums(planning_query(category), period) - end + def sum_total_plannings(period = nil) + query_planning_sums(planning_query(category), period) + end - def planning_query(receiver, division = nil) - query = receiver.plannings. - joins(:work_item). - joins('INNER JOIN accounting_posts ON accounting_posts.work_item_id = ANY (work_items.path_ids)'). - definitive - query = query.where("? = #{category_ref}", category_id) if division && category_ref - query - end + def planning_query(receiver, division = nil) + query = receiver.plannings + .joins(:work_item) + .joins(Arel.sql('INNER JOIN accounting_posts ON accounting_posts.work_item_id = ANY (work_items.path_ids)')) + .definitive + query = query.where("? = #{category_ref}", category_id) if division && category_ref + query + end - ################ Methods for overview ############## + ################ Methods for overview ############## - # The title for this Evaluation - def title - label + (class_category? ? ' Übersicht' : ' von ' + category.label) - end + # The title for this Evaluation + def title + label + (class_category? ? ' Übersicht' : " von #{category.label}") + end - def worktime_name - absences? ? Absencetime.label : Ordertime.label - end + def worktime_name + absences? ? Absencetime.label : Ordertime.label + end - # The header name of the division column to be displayed. - # Returns the class name of the division objects. - def division_header - divs = divisions - divs.respond_to?(:klass) ? divs.klass.model_name.human : '' - end + # The header name of the division column to be displayed. + # Returns the class name of the division objects. + def division_header + divs = divisions + divs.respond_to?(:klass) ? divs.klass.model_name.human : '' + end - # Returns a two-dimensional Array with helper methods of the evaluator - # to be called in the overview (_division.rhtml) for each division - # and the according table headers. May be used for displaying additional - # information or links to certain actions. - # No methods are called by default. - # See EmployeeWorkItemsEval for an example. - def division_supplement(_user) - [] - end + # Returns a two-dimensional Array with helper methods of the evaluator + # to be called in the overview (_division.rhtml) for each division + # and the according table headers. May be used for displaying additional + # information or links to certain actions. + # No methods are called by default. + # See EmployeeWorkItemsEval for an example. + def division_supplement(_user) + [] + end - def sub_work_items_evaluation(division) - sub_work_items_eval if sub_work_items_eval && division.children? - end + def sub_work_items_evaluation(division) + sub_work_items_eval if sub_work_items_eval && division.children? + end - def include_no_period_zero_totals - !category.is_a?(Employee) - end + def include_no_period_zero_totals + !category.is_a?(Employee) + end - # Returns whether this Evaluation is personally for the current user. - # Default is false. - def for?(_user) - false - end + # Returns whether this Evaluation is personally for the current user. + # Default is false. + def for?(_user) + false + end - def ==(other) - self.class == other.class && - category == other.category - end + def ==(other) + self.class == other.class && + category == other.category + end - ################ Methods for detail view ############## + ################ Methods for detail view ############## - # Sets the id of the division object used for the detailed view. - # Default is nil, the worktimes of all divisions are provided. - def set_division_id(division_id = nil) - return if division_id.nil? + # Sets the id of the division object used for the detailed view. + # Default is nil, the worktimes of all divisions are provided. + def set_division_id(division_id = nil) + return if division_id.nil? - container = class_category? ? category : divisions - @division = container.find(division_id.to_i) - end + container = class_category? ? category : divisions + @division = container.find(division_id.to_i) + end - # Label for the represented category. - def category_label - detail_label(category) - end + # Label for the represented category. + def category_label + detail_label(category) + end - # Label for the represented division, if any. - def division_label - detail_label(division) - end + # Label for the represented division, if any. + def division_label + detail_label(division) + end - def edit_link?(user) - for?(user) || !absences? - end + def edit_link?(user) + for?(user) || !absences? + end - def delete_link?(user) - for? user - end + def delete_link?(user) + for? user + end - def action_links?(user) - user.management || edit_link?(user) || delete_link?(user) - end + def action_links?(user) + user.management || edit_link?(user) || delete_link?(user) + end - def report? - account_id && !absences? # && employee_id - end + def report? + account_id && !absences? # && employee_id + end - def employee_id - nil - end + def employee_id + nil + end - def account_id - nil - end + def account_id + nil + end - private + private - # Initializes a new Evaluation with the given category. - def initialize(category) - @category = category - end + # Initializes a new Evaluation with the given category. + def initialize(category, **search_conditions) + @category = category + @search_conditions = search_conditions + end - def query_time_sums(query, group_by_column = nil) - if billable_hours - if group_by_column.present? - query_grouped_time_sums(query, group_by_column) + def query_time_sums(query, group_by_column = nil) + if billable_hours + if group_by_column.present? + query_grouped_time_sums(query, group_by_column) + else + result = query.pick(*hours_and_billable_hours_columns) + { hours: result.first.to_f, billable_hours: result.second.to_f } + end else - result = query.pluck(*hours_and_billable_hours_columns).first - { hours: result.first.to_f, billable_hours: result.second.to_f } + result = query.sum(:hours) + result.is_a?(Hash) ? result : result.to_f end - else - result = query.sum(:hours) - result.is_a?(Hash) ? result : result.to_f end - end - def query_grouped_time_sums(query, group_by_column) - result = query.pluck(group_by_column, *hours_and_billable_hours_columns) - result.each_with_object({}) do |e, h| - h[e[0]] = { hours: e[1].to_f, billable_hours: e[2].to_f } + def query_grouped_time_sums(query, group_by_column) + result = query.pluck(group_by_column, *hours_and_billable_hours_columns) + result.each_with_object({}) do |e, h| + h[e[0]] = { hours: e[1].to_f, billable_hours: e[2].to_f } + end end - end - def hours_and_billable_hours_columns - ['SUM(worktimes.hours) AS sum_hours', - 'SUM(CASE WHEN worktimes.billable = TRUE ' \ - 'THEN worktimes.hours ' \ - 'ELSE 0 END) ' \ - 'AS sum_billable_hours'] - end + def hours_and_billable_hours_columns + [Arel.sql('SUM(worktimes.hours) AS sum_hours'), + Arel.sql('SUM(CASE WHEN worktimes.billable = TRUE ' \ + 'THEN worktimes.hours ' \ + 'ELSE 0 END) ' \ + 'AS sum_billable_hours')] + end - def query_planning_sums(query, period) - { hours: 0, billable_hours: 0 }.tap do |result| - WorkingCondition.each_period_of(:must_hours_per_day, period) do |p, hours| - r = query.in_period(p).pluck(*plannings_and_billable_plannings_columns(hours)).first - result[:hours] += r.first.to_f - result[:billable_hours] += r.second.to_f + def query_planning_sums(query, period) + { hours: 0, billable_hours: 0 }.tap do |result| + WorkingCondition.each_period_of(:must_hours_per_day, period) do |p, hours| + r = query.in_period(p).pick(*plannings_and_billable_plannings_columns(hours)) + result[:hours] += r.first.to_f + result[:billable_hours] += r.second.to_f + end end end - end - def query_grouped_planning_sums(query, period, group_by_column) - {}.tap do |result| - WorkingCondition.each_period_of(:must_hours_per_day, period) do |p, hours| - r = query.in_period(p).pluck(group_by_column, *plannings_and_billable_plannings_columns(hours)) - r.each do |e| - result[e[0]] ||= { hours: 0, billable_hours: 0 } - result[e[0]][:hours] += e[1].to_f - result[e[0]][:billable_hours] += e[2].to_f + def query_grouped_planning_sums(query, period, group_by_column) + {}.tap do |result| + WorkingCondition.each_period_of(:must_hours_per_day, period) do |p, hours| + r = query.in_period(p).pluck(group_by_column, *plannings_and_billable_plannings_columns(hours)) + r.each do |e| + result[e[0]] ||= { hours: 0, billable_hours: 0 } + result[e[0]][:hours] += e[1].to_f + result[e[0]][:billable_hours] += e[2].to_f + end end end end - end - def plannings_and_billable_plannings_columns(must_hours) - ["SUM(plannings.percent / 100.0 * #{must_hours.to_f}) AS sum_hours", - 'SUM(CASE WHEN accounting_posts.billable = TRUE ' \ - "THEN plannings.percent / 100.0 * #{must_hours.to_f} " \ - 'ELSE 0 END) ' \ - 'AS sum_billable_hours'] - end + def plannings_and_billable_plannings_columns(must_hours) + [Arel.sql("SUM(plannings.percent / 100.0 * #{must_hours.to_f}) AS sum_hours"), + Arel.sql('SUM(CASE WHEN accounting_posts.billable = TRUE ' \ + "THEN plannings.percent / 100.0 * #{must_hours.to_f} " \ + 'ELSE 0 END) ' \ + 'AS sum_billable_hours')] + end - def worktime_type - absences? ? 'Absencetime' : 'Ordertime' - end + def worktime_type + absences? ? 'Absencetime' : 'Ordertime' + end - def detail_label(item) - return '' if item.nil? || item.is_a?(Class) + def detail_label(item) + return '' if item.nil? || item.is_a?(Class) - item.class.model_name.human + ': ' + item.label - end + "#{item.class.model_name.human}: #{item.label}" + end - def class_category? - category.is_a? Class + def class_category? + category.is_a? Class + end end end diff --git a/app/domain/evaluations/managed_orders_eval.rb b/app/domain/evaluations/managed_orders_eval.rb index 0b477f688..ab7814dc4 100644 --- a/app/domain/evaluations/managed_orders_eval.rb +++ b/app/domain/evaluations/managed_orders_eval.rb @@ -1,48 +1,52 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class ManagedOrdersEval < WorkItemsEval - self.label = 'Geleitete Aufträge' - self.division_column = 'orders.work_item_id' - self.total_details = false - self.billable_hours = true - self.planned_hours = true +module Evaluations + class ManagedOrdersEval < Evaluations::WorkItemsEval + self.label = 'Geleitete Aufträge' + self.division_column = 'orders.work_item_id' + self.total_details = false + self.billable_hours = true + self.planned_hours = true - def category_label - 'Kunde: ' + division.order.client.name - end + def category_label + "Kunde: #{division.order.client.name}" + end - def divisions(_period = nil) - WorkItem.joins(:order).includes(:order).where(orders: { responsible_id: category.id }).list - end + def divisions(_period = nil) + WorkItem.joins(:order).includes(:order).where(orders: { responsible_id: category.id }).list + end - def division_supplement(_user) - [[:order_completed, 'Abschluss erledigt', 'left'], - [:order_committed, 'Abschluss freigegeben', 'left']] - end + def division_supplement(_user) + [[:order_completed, 'Abschluss erledigt', 'left'], + [:order_committed, 'Abschluss freigegeben', 'left']] + end + + private - private - - def worktime_query(receiver, period = nil, division = nil) - if receiver == category - Worktime. - joins(:work_item). - joins('INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)'). - where(type: 'Ordertime'). - where(orders: { responsible_id: category.id }). - in_period(period) - else - super(receiver, period, division) + def worktime_query(receiver, period = nil, division = nil) + if receiver == category + Worktime + .joins(:work_item) + .joins('INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)') + .where(type: 'Ordertime') + .where(orders: { responsible_id: category.id }) + .in_period(period) + else + super(receiver, period, division) + end end - end - def planning_query(_receiver, _division = nil) - Planning. - joins(:work_item). - joins('INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)'). - joins('INNER JOIN accounting_posts ON accounting_posts.work_item_id = ANY (work_items.path_ids)'). - where(orders: { responsible_id: category.id }) + def planning_query(_receiver, _division = nil) + Planning + .joins(:work_item) + .joins('INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)') + .joins('INNER JOIN accounting_posts ON accounting_posts.work_item_id = ANY (work_items.path_ids)') + .where(orders: { responsible_id: category.id }) + end end end diff --git a/app/domain/evaluations/sub_work_items_eval.rb b/app/domain/evaluations/sub_work_items_eval.rb index 4d5aa4525..0ffdabe21 100644 --- a/app/domain/evaluations/sub_work_items_eval.rb +++ b/app/domain/evaluations/sub_work_items_eval.rb @@ -1,33 +1,37 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class SubWorkItemsEval < WorkItemsEval - self.division_method = :children - self.label = 'Subpositionen' - self.billable_hours = true - self.planned_hours = true +module Evaluations + class SubWorkItemsEval < Evaluations::WorkItemsEval + self.division_method = :children + self.label = 'Subpositionen' + self.billable_hours = true + self.planned_hours = true - def initialize(item_id) - super(WorkItem.find(item_id)) - end + def initialize(item_id) + super(WorkItem.find(item_id)) + end - def account_id - division ? division.id : category.id - end + def account_id + division ? division.id : category.id + end - # Label for the represented category. - def category_label - 'Kunde: ' + category.top_item.client.label - end + # Label for the represented category. + def category_label + "Kunde: #{category.top_item.client.label}" + end - # Label for the represented division, if any. - def division_label - 'Position: ' + (division ? division : category).label_ancestry - end + # Label for the represented division, if any. + def division_label + "Position: #{(division || category).label_ancestry}" + end - def division_column - "work_items.path_ids[#{category.path_ids.size + 1}]" + def division_column + Arel.sql("work_items.path_ids[#{category.path_ids.size + 1}]") + end end end diff --git a/app/domain/evaluations/work_item_employees_eval.rb b/app/domain/evaluations/work_item_employees_eval.rb index c505afd73..e5bbf9767 100644 --- a/app/domain/evaluations/work_item_employees_eval.rb +++ b/app/domain/evaluations/work_item_employees_eval.rb @@ -1,48 +1,52 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class WorkItemEmployeesEval < Evaluation - self.division_method = :employees - self.division_column = :employee_id - self.label = 'Members' - self.category_ref = 'ANY ( path_ids )' - self.billable_hours = true - self.planned_hours = true - - def initialize(item_id) - super(WorkItem.find(item_id)) - end - - def employee_id - division.id if division - end - - def account_id - category.id - end - - ####### overwritten methods for working with work item hierarchies - - def category_label - "Position: #{category.top? ? category.label : category.label_verbose}" - end - - def worktime_query(receiver, period = nil, division = nil) - super(receiver, period, division).joins(:work_item) - end - - def set_division_id(division_id = nil) - return if division_id.nil? - - @division = Employee.find(division_id.to_i) - end - - def division_supplement(_user) - [ - [:worktime_commits_readonly, 'Freigabe', 'left'], - [:worktime_reviews_readonly, 'Kontrolle', 'left'] - ] +module Evaluations + class WorkItemEmployeesEval < Evaluations::Evaluation + self.division_method = :employees + self.division_column = :employee_id + self.label = 'Members' + self.category_ref = 'ANY ( path_ids )' + self.billable_hours = true + self.planned_hours = true + + def initialize(item_id) + super(WorkItem.find(item_id)) + end + + def employee_id + division&.id + end + + def account_id + category.id + end + + ####### overwritten methods for working with work item hierarchies + + def category_label + "Position: #{category.top? ? category.label : category.label_verbose}" + end + + def worktime_query(receiver, period = nil, division = nil) + super(receiver, period, division).joins(:work_item) + end + + def set_division_id(division_id = nil) + return if division_id.nil? + + @division = Employee.find(division_id.to_i) + end + + def division_supplement(_user) + [ + [:worktime_commits_readonly, 'Freigabe', 'left'], + [:worktime_reviews_readonly, 'Kontrolle', 'left'] + ] + end end end diff --git a/app/domain/evaluations/work_items_eval.rb b/app/domain/evaluations/work_items_eval.rb index ae2e889f8..43d02db6a 100644 --- a/app/domain/evaluations/work_items_eval.rb +++ b/app/domain/evaluations/work_items_eval.rb @@ -1,18 +1,22 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. # abstract class for evaluation with work item divisions -class WorkItemsEval < Evaluation - self.division_method = :work_items - self.division_column = 'work_items.path_ids[1]' - self.division_join = :work_item - self.label = 'Positionen' - self.sub_evaluation = 'workitememployees' - self.sub_work_items_eval = 'subworkitems' +module Evaluations + class WorkItemsEval < Evaluations::Evaluation + self.division_method = :work_items + self.division_column = Arel.sql('work_items.path_ids[1]') + self.division_join = :work_item + self.label = 'Positionen' + self.sub_evaluation = 'workitememployees' + self.sub_work_items_eval = 'subworkitems' - def account_id - division.id if division + def account_id + division&.id + end end end diff --git a/app/domain/expenses/pdf_export.rb b/app/domain/expenses/pdf_export.rb index 8ec6f3b95..4392d347c 100644 --- a/app/domain/expenses/pdf_export.rb +++ b/app/domain/expenses/pdf_export.rb @@ -5,203 +5,204 @@ # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Expenses::PdfExport - include ActiveStorage::Downloading +module Expenses + class PdfExport + require 'mini_magick' - require 'mini_magick' + attr_accessor :pdf + attr_reader :expense, :entries - attr_accessor :pdf - attr_reader :expense, :entries + FILENAME = 'tmp/expenses.pdf' - FILENAME = 'tmp/expenses.pdf' + def initialize(entries) + @pdf = Prawn::Document.new(page_size: 'A4') + @entries = entries + end - def initialize(entries) - @pdf = Prawn::Document.new(page_size: 'A4') - @entries = entries - end + def expenses + @expenses ||= + Expense + .includes(:employee, :reviewer, :reviewer) + .includes(order: :work_item, receipt_attachment: [{ blob: [{ preview_image_attachment: :blob }] }]) + .where(id: entries) + .order('employees.lastname') + .order(:payment_date) + end - def expenses - @expenses ||= - Expense - .includes(:employee, :reviewer, :reviewer) - .includes(order: :work_item, receipt_attachment: [{ blob: [{ preview_image_attachment: :blob }] }]) - .where(id: entries) - .order('employees.lastname') - .order(:payment_date) - end + def build + validate + setup_fonts + expenses.each_with_index do |e, i| + @expense = e + pdf.start_new_page unless i.zero? + add_header + add_receipt + reset_model_data + end + pdf.number_pages('Seite /', at: [pdf.bounds.right - 60, pdf.bounds.bottom + 5]) + end - def build - validate - setup_fonts - expenses.each_with_index do |e, i| - @expense = e - pdf.start_new_page unless i.zero? - add_header - add_receipt - reset_model_data - end - pdf.number_pages('Seite /', at: [pdf.bounds.right - 60, pdf.bounds.bottom + 5]) - end + def generate + build + pdf.render_file FILENAME + FILENAME + end - def generate - build - pdf.render_file FILENAME - FILENAME - end + private - private + def validate + raise ArgumentError, 'There are no approved expenses.' if expenses.blank? - def validate - raise ArgumentError, 'There are no approved expenses.' if expenses.blank? + true + end - true - end + def setup_fonts + pdf.font_families.update( + 'Roboto' => { + normal: 'app/assets/fonts/Roboto-Regular.ttf', + italic: 'app/assets/fonts/Roboto-Italic.ttf', + bold: 'app/assets/fonts/Roboto-Bold.ttf', + bold_italic: 'app/assets/fonts/Roboto-BoldItalic.ttf' + + } + ) + pdf.font('Roboto') + end - def setup_fonts - pdf.font_families.update( - 'Roboto' => { - normal: 'app/assets/fonts/Roboto-Regular.ttf', - italic: 'app/assets/fonts/Roboto-Italic.ttf', - bold: 'app/assets/fonts/Roboto-Bold.ttf', - bold_italic: 'app/assets/fonts/Roboto-BoldItalic.ttf' - - } - ) - pdf.font('Roboto') - end + def add_header + # pdf.column_box([pdf.bounds.left, pdf.bounds.top], columns: 3, width: pdf.bounds.width, height: 100) do + # add_model_data + # end + pdf.bounding_box([pdf.bounds.left, pdf.bounds.top], width: pdf.bounds.width, height: 150) do + pdf.define_grid(columns: 2, rows: 1) - def add_header - # pdf.column_box([pdf.bounds.left, pdf.bounds.top], columns: 3, width: pdf.bounds.width, height: 100) do - # add_model_data - # end - pdf.bounding_box([pdf.bounds.left, pdf.bounds.top], width: pdf.bounds.width, height: 150) do - pdf.define_grid(columns: 2, rows: 1) + column1 = [:employee_id, :kind, :order_id, :status, nil, nil, { name: :amount, size: 15 }] + column2 = %i[id reviewer_id reviewed_at reason reimbursement_month payment_date description receipt] - column1 = [:employee_id, :kind, :order_id, :status, nil, nil, { name: :amount, size: 15 }] - column2 = [:id, :reviewer_id, :reviewed_at, :reason, :reimbursement_month, :payment_date, :description, :receipt] + pdf.grid(0, 0).bounding_box do + add_model_data(column1) + end - pdf.grid(0, 0).bounding_box do - add_model_data(column1) + pdf.grid(0, 1).bounding_box do + add_model_data(column2) + end end + end - pdf.grid(0, 1).bounding_box do - add_model_data(column2) + def add_model_data(items) + items.each do |item| + add_text ' ' unless item + add_single_model_data item if item.is_a? Symbol + add_single_model_data item[:name], size: item[:size] if item.is_a? Hash end end - end - def add_model_data(items) - items.each do |item| - add_text ' ' unless item - add_single_model_data item if item.is_a? Symbol - add_single_model_data item[:name], size: item[:size] if item.is_a? Hash + def add_single_model_data(key, **) + add_text(attribute(key, model_data[key]), **) end - end - def add_single_model_data(key, **options) - add_text attribute(key, model_data[key]), options - end + def add_text(text, **options) + options[:inline_format] = true + pdf.text text, options + end - def add_text(text, **options) - options[:inline_format] = true - pdf.text text, options - end + def format_employee(employee) + "#{employee.firstname} #{employee.lastname}" if employee + end - def format_employee(employee) - "#{employee.firstname} #{employee.lastname}" if employee - end + def format_value + format( + '%0.02f %s', + amount: expense.amount, + currency: Settings.defaults.currency + ) + end - def format_value - format( - '%0.02f %s', - amount: expense.amount, - currency: Settings.defaults.currency - ) - end + def format_order + expense.order&.path_shortnames + end - def format_order - expense.order&.path_shortnames - end + def format_date(date) + I18n.l(date, format: '%d.%m.%Y') if date + end - def format_date(date) - I18n.l(date, format: '%d.%m.%Y') if date - end + def model_data + @model_data ||= + begin + output = {} + output[:employee_id] = format_employee(expense.employee) + output[:status] = expense.status_value + output[:kind] = expense.kind_value + output[:order_id] = format_order if expense.project? + output[:reviewer_id] = format_employee(expense.reviewer) if expense.approved? || expense.rejected? + output[:reviewed_at] = format_date(expense.reviewed_at) if expense.approved? || expense.rejected? + output[:reimbursement_month] = expense.reimbursement_month if expense.approved? + output[:reason] = expense.reason&.truncate(90) if expense.approved? || expense.rejected? + output[:id] = expense.id + output[:amount] = format_value + output[:payment_date] = format_date(expense.payment_date) + output[:description] = expense.description&.truncate(90) if expense.description + output[:receipt] = receipt_text + output + end + end - def model_data - @model_data ||= - begin - output = {} - output[:employee_id] = format_employee(expense.employee) - output[:status] = expense.status_value - output[:kind] = expense.kind_value - output[:order_id] = format_order if expense.project? - output[:reviewer_id] = format_employee(expense.reviewer) if (expense.approved? || expense.rejected?) - output[:reviewed_at] = format_date(expense.reviewed_at) if (expense.approved? || expense.rejected?) - output[:reimbursement_month] = expense.reimbursement_month if expense.approved? - output[:reason] = expense.reason&.truncate(90) if (expense.approved? || expense.rejected?) - output[:id] = expense.id - output[:amount] = format_value - output[:payment_date] = format_date(expense.payment_date) - output[:description] = expense.description&.truncate(90) if expense.description - output[:receipt] = receipt_text - output - end - end + def reset_model_data + @model_data = nil + end - def reset_model_data - @model_data = nil - end + def attribute(title, value) + "#{t(title)}: #{value}" + end - def attribute(title, value) - "#{t(title)}: #{value}" - end + def receipt_printable? + receipt.attached? && receipt.image? # Currently, previewables will not be handled + end - def receipt_printable? - receipt.attached? && receipt.image? # Currently, previewables will not be handled - end + def receipt + expense.receipt + end - def receipt - expense.receipt - end + def receipt_text + return receipt.filename if receipt_printable? + return 'Dieser Beleg kann nicht gedruckt werden.' if receipt.attached? - def receipt_text - return receipt.filename if receipt_printable? - return 'Dieser Beleg kann nicht gedruckt werden.' if receipt.attached? + 'Es wurde kein Beleg beigelegt.' + end - 'Es wurde kein Beleg beigelegt.' - end + def add_receipt + return unless receipt_printable? - def add_receipt - return unless receipt_printable? + blob.open do |file| + # Vips auto rotates by default + image = ::Vips::Image.new_from_file(file.path) + rotated = ImageProcessing::Vips.source(image) + rotated.write_to_file(file.path) + pdf.image file.path, position: :center, fit: [image_width, image_height] + end + rescue StandardError => e + add_text "Error while adding picture for expense #{@expense.id}" + add_text "Message: #{e.message}" - download_blob_to_tempfile do |file| - image = ::MiniMagick::Image.open(file.path) - image.auto_orient - image.write file.path - pdf.image file.path, position: :center, fit: [image_width, image_height] + Rails.logger.info "Error while adding picture for expense #{@expense.id}" + Rails.logger.info "Message: #{e.message}" + Rails.logger.info "Backtrace: #{e.backtrace.inspect}" end - rescue StandardError => e - add_text "Error while adding picture for expense #{@expense.id}" - add_text "Message: #{e.message}" - Rails.logger.info "Error while adding picture for expense #{@expense.id}" - Rails.logger.info "Message: #{e.message}" - Rails.logger.info "Backtrace: #{e.backtrace.inspect}" - end - - def blob - receipt.blob - end + def blob + receipt.blob + end - def image_width - pdf.bounds.width - end + def image_width + pdf.bounds.width + end - def image_height - pdf.bounds.height - (pdf.bounds.height - pdf.cursor) - end + def image_height + pdf.bounds.height - (pdf.bounds.height - pdf.cursor) + end - def t(label, **kwargs) - I18n.t("activerecord.attributes.expense.#{label}", kwargs) + def t(label, **) + I18n.t("activerecord.attributes.expense.#{label}", **) + end end end diff --git a/app/domain/forms/multi_absence.rb b/app/domain/forms/multi_absence.rb index a018d6a8f..683a10559 100644 --- a/app/domain/forms/multi_absence.rb +++ b/app/domain/forms/multi_absence.rb @@ -1,92 +1,92 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class MultiAbsence - attr_reader :absence_id, :employee, :work_date, :duration, :description, :worktime - attr_writer :employee +module Forms + class MultiAbsence + attr_accessor :employee + attr_reader :absence_id, :work_date, :duration, :description, :worktime - def initialize - @duration = 1 - end + def initialize + @duration = 1 + end - def attributes=(attr_hash) - @absence_id = attr_hash[:absence_id] - @work_date = attr_hash[:work_date] - @duration = attr_hash[:duration] - @description = attr_hash[:description] - end + def attributes=(attr_hash) + @absence_id = attr_hash[:absence_id] + @work_date = attr_hash[:work_date] + @duration = attr_hash[:duration] + @description = attr_hash[:description] + end - def valid? - @worktime = worktime_template(@work_date, - WorkingCondition.value_at(work_date, :must_hours_per_day)) - valid = @worktime.valid? - if valid - if duration <= 0 + def valid? + @worktime = worktime_template(@work_date, + WorkingCondition.value_at(work_date, :must_hours_per_day)) + valid = @worktime.valid? + if valid && (duration <= 0) valid = false @worktime.errors.add(:work_date, 'Die Dauer muss grösser als 0 sein.') end + valid end - valid - end - def work_date - date_or_nil(@work_date) - end + def work_date + date_or_nil(@work_date) + end - def end_date - work_date + duration * 7 - 1 - end + def end_date + work_date + (duration * 7) - 1 + end - def duration - @duration.to_i - end + def duration + @duration.to_i + end - def period - Period.new(work_date, end_date) - end + def period + Period.new(work_date, end_date) + end - def errors - @worktime ? @worktime.errors : {} - end + def errors + @worktime ? @worktime.errors : {} + end - def save - absences = [] - period.step do |date| - employment = @employee.employment_at(date) - if employment - must = Holiday.musttime(date) * employment.percent_factor - if must > 0 - absences << worktime_template(date, must) + def save + absences = [] + period.step do |date| + employment = @employee.employment_at(date) + if employment + must = Holiday.musttime(date) * employment.percent_factor + absences << worktime_template(date, must) if must.positive? end end + absences.each(&:save) + absences end - absences.each(&:save) - absences - end - private + private - def date_or_nil(value) - unless value.is_a? Date - begin - value = Date.parse(value) - rescue - value = nil + def date_or_nil(value) + unless value.is_a? Date + begin + value = Date.parse(value) + rescue StandardError + value = nil + end end + value end - value - end - def worktime_template(date, hours) - worktime = Absencetime.new - worktime.report_type = HoursDayType::INSTANCE - worktime.work_date = date - worktime.absence_id = @absence_id - worktime.description = @description - worktime.employee = @employee - worktime.hours = hours - worktime + def worktime_template(date, hours) + worktime = Absencetime.new + worktime.report_type = ReportType::HoursDayType::INSTANCE + worktime.work_date = date + worktime.absence_id = @absence_id + worktime.description = @description + worktime.employee = @employee + worktime.hours = hours + worktime + end end end diff --git a/app/domain/forms/splitable.rb b/app/domain/forms/splitable.rb index 5b98c03e3..11811e6d1 100644 --- a/app/domain/forms/splitable.rb +++ b/app/domain/forms/splitable.rb @@ -1,69 +1,71 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Splitable - class_attribute :incomplete_finish - self.incomplete_finish = true +module Forms + class Splitable + class_attribute :incomplete_finish + self.incomplete_finish = true - attr_reader :original, :original_id, :worktimes + attr_reader :original, :original_id, :worktimes - def initialize(original) - @original = original.dup - @original_id = original.id - @worktimes = [] - end + def initialize(original) + @original = original.dup + @original_id = original.id + @worktimes = [] + end - def add_worktime(worktime) - @worktimes.push(worktime) - end + def add_worktime(worktime) + @worktimes.push(worktime) + end - def remove_worktime(index) - @worktimes.delete_at(index) if @worktimes[index].new_record? - end + def remove_worktime(index) + @worktimes.delete_at(index) if @worktimes[index].new_record? + end - def build_worktime - Ordertime.new - end + def build_worktime + Ordertime.new + end - def worktime_template - worktime = last_worktime.template Ordertime.new - worktime.hours = remaining_hours - worktime.from_start_time = next_start_time - worktime.to_end_time = original.to_end_time if next_start_time - worktime - end + def worktime_template + worktime = last_worktime.template Ordertime.new + worktime.hours = remaining_hours + worktime.from_start_time = next_start_time + worktime.to_end_time = original.to_end_time if next_start_time + worktime + end - def complete? - remaining_hours < 0.00001 # we are working with floats: use delta - end + def complete? + remaining_hours < 0.00001 # we are working with floats: use delta + end - def save - Worktime.transaction do - worktimes.each(&:save!) + def save + Worktime.transaction do + worktimes.each(&:save!) + end end - end - def page_title - 'Aufteilen' - end + def page_title + 'Aufteilen' + end - def empty? - worktimes.empty? - end + delegate :empty?, to: :worktimes - protected + protected - def remaining_hours - original.hours - worktimes.inject(0) { |sum, time| sum + time.hours } - end + def remaining_hours + original.hours - worktimes.inject(0) { |sum, time| sum + time.hours } + end - def next_start_time - empty? ? original.from_start_time : worktimes.last.to_end_time - end + def next_start_time + empty? ? original.from_start_time : worktimes.last.to_end_time + end - def last_worktime - empty? ? original : worktimes.last + def last_worktime + empty? ? original : worktimes.last + end end end diff --git a/app/domain/forms/worktime_edit.rb b/app/domain/forms/worktime_edit.rb index 1e70a0cb5..fd722434c 100644 --- a/app/domain/forms/worktime_edit.rb +++ b/app/domain/forms/worktime_edit.rb @@ -1,25 +1,29 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class WorktimeEdit < Splitable - self.incomplete_finish = false +module Forms + class WorktimeEdit < Forms::Splitable + self.incomplete_finish = false - def add_worktime(worktime) - if worktime.hours - remaining_hours > 0.00001 # we are working with floats: use delta - worktime.errors.add(:hours, 'Die gesamte Anzahl Stunden kann nicht vergrössert werden') + def add_worktime(worktime) + if worktime.hours - remaining_hours > 0.00001 # we are working with floats: use delta + worktime.errors.add(:hours, 'Die gesamte Anzahl Stunden kann nicht vergrössert werden') + end + worktime.employee = original.employee + super(worktime) if worktime.errors.empty? + worktime.errors.empty? end - worktime.employee = original.employee - super(worktime) if worktime.errors.empty? - worktime.errors.empty? - end - def page_title - "Arbeitszeit von #{original.employee.label} bearbeiten" - end + def page_title + "Arbeitszeit von #{original.employee.label} bearbeiten" + end - def build_worktime - empty? ? Ordertime.find(original_id) : Ordertime.new + def build_worktime + empty? ? Ordertime.find(original_id) : Ordertime.new + end end end diff --git a/app/domain/graphs/account_color_mapper.rb b/app/domain/graphs/account_color_mapper.rb index deac49176..82cf7f6c3 100644 --- a/app/domain/graphs/account_color_mapper.rb +++ b/app/domain/graphs/account_color_mapper.rb @@ -1,53 +1,57 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class AccountColorMapper - def initialize - @map = {} - end +module Graphs + class AccountColorMapper + def initialize + @map = {} + end - def [](account) - @map[account] ||= generate_color account - end + def [](account) + @map[account] ||= generate_color account + end - def accounts?(type) - !accounts(type).empty? - end + def accounts?(type) + !accounts(type).empty? + end - def accounts_legend(type) - accounts = accounts(type).sort - accounts.collect { |p| [p.label_verbose, @map[p]] } - end + def accounts_legend(type) + accounts = accounts(type).sort + accounts.collect { |p| [p.label_verbose, @map[p]] } + end - private + private - def generate_color(account) - if account.is_a?(Absence) - generate_absence_color(account.id) - else - generate_work_item_color(account.id) + def generate_color(account) + if account.is_a?(Absence) + generate_absence_color(account.id) + else + generate_work_item_color(account.id) + end end - end - def generate_absence_color(id) - srand id - '#FF' + random_color(230) + random_color(140) - end + def generate_absence_color(id) + srand id + "#FF#{random_color(230)}#{random_color(140)}" + end - def generate_work_item_color(id) - srand id - '#' + random_color(170) + random_color(240) + 'FF' - end + def generate_work_item_color(id) + srand id + "##{random_color(170)}#{random_color(240)}FF" + end - def random_color(span = 170) - lower = (255 - span) / 2 - hex = (lower + rand(span)).to_s(16) - hex.size == 1 ? '0' + hex : hex - end + def random_color(span = 170) + lower = (255 - span) / 2 + hex = (lower + rand(span)).to_s(16) + hex.size == 1 ? "0#{hex}" : hex + end - def accounts(type) - @map.keys.select { |key| key.is_a? type } + def accounts(type) + @map.keys.select { |key| key.is_a? type } + end end end diff --git a/app/domain/graphs/timebox.rb b/app/domain/graphs/timebox.rb index 345cb3d36..ba17170d8 100644 --- a/app/domain/graphs/timebox.rb +++ b/app/domain/graphs/timebox.rb @@ -1,57 +1,61 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Timebox - PIXEL_PER_HOUR = 8.0 +module Graphs + class Timebox + PIXEL_PER_HOUR = 8.0 - MUST_HOURS_COLOR = '#FF0000'.freeze - BLANK_COLOR = 'transparent'.freeze + MUST_HOURS_COLOR = '#FF0000' + BLANK_COLOR = 'transparent' - attr_reader :height, :color, :tooltip, :worktime - attr_writer :height, :worktime + attr_accessor :height, :worktime + attr_reader :color, :tooltip - include ActionView::Helpers::OutputSafetyHelper + include ActionView::Helpers::OutputSafetyHelper - class << self - def must_hours(must_hours) - new(nil, MUST_HOURS_COLOR, 1, 'Sollzeit ('.html_safe << format_hour(must_hours) << ')') - end + class << self + def must_hours(must_hours) + new(nil, MUST_HOURS_COLOR, 1, 'Sollzeit ('.html_safe << format_hour(must_hours) << ')') + end - def blank(hours) - new(nil, BLANK_COLOR, height_from_hours(hours), '') - end + def blank(hours) + new(nil, BLANK_COLOR, height_from_hours(hours), '') + end - def height_from_hours(hours) - # Round to fix pixel display issues - rounded = (hours * 4).round / 4.0 - rounded * PIXEL_PER_HOUR - end + def height_from_hours(hours) + # Round to fix pixel display issues + rounded = (hours * 4).round / 4.0 + rounded * PIXEL_PER_HOUR + end - def format_hour(hour) - ActionController::Base.helpers.number_with_precision(hour, precision: 2, delimiter: '\'') + def format_hour(hour) + ActionController::Base.helpers.number_with_precision(hour, precision: 2, delimiter: '\'') + end end - end - def initialize(worktime, color = nil, hgt = nil, tooltip = nil) - if worktime - @worktime = worktime - hgt ||= self.class.height_from_hours worktime.hours - tooltip ||= tooltip_for worktime + def initialize(worktime, color = nil, hgt = nil, tooltip = nil) + if worktime + @worktime = worktime + hgt ||= self.class.height_from_hours worktime.hours + tooltip ||= tooltip_for worktime + end + @height = (hgt * 10).round / 10.0 + @color = color + @tooltip = tooltip end - @height = (hgt * 10).round / 10.0 - @color = color - @tooltip = tooltip - end - def stretch(factor) - @height *= factor - end + def stretch(factor) + @height *= factor + end - private + private - def tooltip_for(worktime) - Timebox.format_hour(worktime.hours) << ': ' << worktime.account.label_verbose + def tooltip_for(worktime) + Graphs::Timebox.format_hour(worktime.hours) << ': ' << worktime.account.label_verbose + end end end diff --git a/app/domain/graphs/vacation_graph.rb b/app/domain/graphs/vacation_graph.rb index 49fdf2f25..536b8b170 100644 --- a/app/domain/graphs/vacation_graph.rb +++ b/app/domain/graphs/vacation_graph.rb @@ -1,228 +1,231 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class VacationGraph - attr_reader :period, :day +module Graphs + class VacationGraph + attr_reader :period, :day - UNPAID_ABSENCE = Absence.new name: 'Unbezahlter Urlaub' - UNPAID_ABSENCE.id = 0 + UNPAID_ABSENCE = Absence.new name: 'Unbezahlter Urlaub' + UNPAID_ABSENCE.id = 0 - def initialize(period = nil) - period ||= Period.current_year - @actual_period = period - @period = period.extend_to_weeks - @todays_week = Period.week_for(Time.zone.today).to_s + def initialize(period = nil) + period ||= Period.current_year + @actual_period = period + @period = period.extend_to_weeks + @todays_week = Period.week_for(Time.zone.today).to_s - @absences_eval = AbsencesEval.new + @absences_eval = Evaluations::AbsencesEval.new - @color_map = AccountColorMapper.new - end + @color_map = Graphs::AccountColorMapper.new + end - def each_employee - Employee.employed_ones(@actual_period).each do |empl| - @absences_eval.set_division_id empl.id - # trade some memory for speed - @absencetimes = @absences_eval.times(period). - reorder('work_date, from_start_time, employee_id, absence_id'). - includes(:absence). - references(:absence). - where('report_type = ? OR report_type = ? OR report_type = ?', - StartStopType::INSTANCE.key, - HoursDayType::INSTANCE.key, - HoursWeekType::INSTANCE.key) - @monthly_absencetimes = @absences_eval.times(period). - reorder('work_date, from_start_time, employee_id, absence_id'). - includes(:absence). - references(:absence). - where('report_type = ?', - HoursMonthType::INSTANCE.key) - @unpaid_absences = empl.statistics.employments_during(period).select { |e| e.percent.zero? } - @unpaid_absences.collect! { |e| Period.new(e.start_date, e.end_date ? e.end_date : period.end_date) } - @index = 0 - @monthly_index = 0 - @unpaid_index = 0 - @month = nil - yield empl + def each_employee + Employee.employed_ones(@actual_period).each do |empl| + @absences_eval.set_division_id empl.id + # trade some memory for speed + @absencetimes = @absences_eval.times(period) + .reorder('work_date, from_start_time, employee_id, absence_id') + .includes(:absence) + .references(:absence) + .where('report_type = ? OR report_type = ? OR report_type = ?', + ReportType::StartStopType::INSTANCE.key, + ReportType::HoursDayType::INSTANCE.key, + ReportType::HoursWeekType::INSTANCE.key) + @monthly_absencetimes = @absences_eval.times(period) + .reorder('work_date, from_start_time, employee_id, absence_id') + .includes(:absence) + .references(:absence) + .where(report_type: ReportType::HoursMonthType::INSTANCE.key) + @unpaid_absences = empl.statistics.employments_during(period).select { |e| e.percent.zero? } + @unpaid_absences.collect! { |e| Period.new(e.start_date, e.end_date || period.end_date) } + @index = 0 + @monthly_index = 0 + @unpaid_index = 0 + @month = nil + yield empl + end end - end - def each_day - @period.step do |day| - @current = get_period_week(day) - yield day + def each_day + @period.step do |day| + @current = get_period_week(day) + yield day + end end - end - def each_week - @period.step(7) do |week| - @current = get_period_week(week) - yield week + def each_week + @period.step(7) do |week| + @current = get_period_week(week) + yield week + end end - end - def timebox - times = Hash.new(0) - absences = add_absences times, @current - tooltip = create_tooltip(absences) - absences = add_monthly_absences times - tooltip += '
'.html_safe unless tooltip.empty? - tooltip += create_tooltip(absences) - tooltip += '
'.html_safe if !tooltip.empty? && !absences.empty? - tooltip += add_unpaid_absences times + def timebox + times = Hash.new(0) + absences = add_absences times, @current + tooltip = create_tooltip(absences) + absences = add_monthly_absences times + tooltip += '
'.html_safe unless tooltip.empty? + tooltip += create_tooltip(absences) + tooltip += '
'.html_safe if !tooltip.empty? && !absences.empty? + tooltip += add_unpaid_absences times - return nil if times.blank? + return nil if times.blank? - max_absence = get_max_absence(times) + max_absence = get_max_absence(times) - hours = times.values.sum / WorkingCondition.value_at(@current.start_date, :must_hours_per_day) - color = color_for(max_absence) - Timebox.new nil, color, hours, tooltip - end + hours = times.values.sum / WorkingCondition.value_at(@current.start_date, :must_hours_per_day) + color = color_for(max_absence) + Graphs::Timebox.new nil, color, hours, tooltip + end - def employee - @absences_eval.division - end + def employee + @absences_eval.division + end - def previous_left_vacations - employee.statistics.remaining_vacations(@actual_period.start_date - 1).round(1) - end + def previous_left_vacations + employee.statistics.remaining_vacations(@actual_period.start_date - 1).round(1) + end - def following_left_vacations - employee.statistics.remaining_vacations(@actual_period.end_date).round(1) - end + def following_left_vacations + employee.statistics.remaining_vacations(@actual_period.end_date).round(1) + end - def granted_vacations - employee.statistics.total_vacations(@actual_period).round(1) - end + def granted_vacations + employee.statistics.total_vacations(@actual_period).round(1) + end - def used_vacations - employee.statistics.used_vacations(@actual_period).round(1) - end + def used_vacations + employee.statistics.used_vacations(@actual_period).round(1) + end - def accounts?(type = Absence) - @color_map.accounts?(type) - end + def accounts?(type = Absence) + @color_map.accounts?(type) + end - def accounts_legend(type = Absence) - @color_map.accounts_legend(type) - end + def accounts_legend(type = Absence) + @color_map.accounts_legend(type) + end - def current_week? - @current.to_s == @todays_week - end + def current_week? + @current.to_s == @todays_week + end - private + private - def add_absences(times, period = @current, monthly = false, factor = 1) - absences = monthly ? monthly_absences_during(period) : absences_during(period) - absences.each do |time| - times[time.absence] += time.hours * factor + def add_absences(times, period = @current, monthly = false, factor = 1) + absences = monthly ? monthly_absences_during(period) : absences_during(period) + absences.each do |time| + times[time.absence] += time.hours * factor + end + absences end - absences - end - def add_monthly_absences(times) - if @current.start_date.month == @current.end_date.month - add_monthly times, @current - else - part1 = add_monthly times, get_period(@current.start_date, @current.start_date.end_of_month) - part2 = add_monthly times, get_period(@current.end_date.beginning_of_month, @current.end_date) - part1 ||= [] - part2 ||= [] - part1.concat part2 + def add_monthly_absences(times) + if @current.start_date.month == @current.end_date.month + add_monthly times, @current + else + part1 = add_monthly times, get_period(@current.start_date, @current.start_date.end_of_month) + part2 = add_monthly times, get_period(@current.end_date.beginning_of_month, @current.end_date) + part1 ||= [] + part2 ||= [] + part1.concat part2 + end end - end - def add_unpaid_absences(times) - tooltip = ''.html_safe - @unpaid_absences.each do |unpaid| - @current.step do |date| - if unpaid.include?(date) && date.wday > 0 && date.wday < 6 - must = WorkingCondition.value_at(date, :must_hours_per_day) - times[UNPAID_ABSENCE] += must - tooltip += "#{I18n.l(date)}: #{must} h #{UNPAID_ABSENCE.label}
".html_safe + def add_unpaid_absences(times) + tooltip = ''.html_safe + @unpaid_absences.each do |unpaid| + @current.step do |date| + if unpaid.include?(date) && date.wday.positive? && date.wday < 6 + must = WorkingCondition.value_at(date, :must_hours_per_day) + times[UNPAID_ABSENCE] += must + tooltip += "#{I18n.l(date)}: #{must} h #{UNPAID_ABSENCE.label}
".html_safe + end end end + tooltip end - tooltip - end - def add_monthly(times, period) - month = get_period_month(period.start_date) - factor = period.musttime.to_f / month.musttime.to_f - add_absences(times, month, true, factor) if factor > 0 - end + def add_monthly(times, period) + month = get_period_month(period.start_date) + factor = period.musttime.to_f / month.musttime + add_absences(times, month, true, factor) if factor.positive? + end - def absences_during(period) - list = iterated_absences(period, @absencetimes, @index) - @index += list.size - list - end + def absences_during(period) + list = iterated_absences(period, @absencetimes, @index) + @index += list.size + list + end - def monthly_absences_during(period) - return @monthly_list if @month == period + def monthly_absences_during(period) + return @monthly_list if @month == period - @monthly_list = iterated_absences(period, @monthly_absencetimes, @monthly_index) - @month = period - @monthly_index += @monthly_list.size - @monthly_list - end + @monthly_list = iterated_absences(period, @monthly_absencetimes, @monthly_index) + @month = period + @monthly_index += @monthly_list.size + @monthly_list + end - def iterated_absences(period, collection, index) - return [] if index >= collection.size || collection[index].work_date > period.end_date + def iterated_absences(period, collection, index) + return [] if index >= collection.size || collection[index].work_date > period.end_date - list = [] - while index < collection.size && collection[index].work_date <= period.end_date - list.push collection[index] - index += 1 + list = [] + while index < collection.size && collection[index].work_date <= period.end_date + list.push collection[index] + index += 1 + end + list end - list - end - def get_max_absence(times) - times.invert[times.values.max] - end + def get_max_absence(times) + times.invert[times.values.max] + end - def create_tooltip(absences) - entries = absences.collect do |time| - "#{I18n.l(time.work_date)}: #{time.time_string} Abwesenheit" + def create_tooltip(absences) + entries = absences.collect do |time| + "#{I18n.l(time.work_date)}: #{time.time_string} Abwesenheit" + end + entries.join('
').html_safe end - entries.join('
').html_safe - end - def color_for(absence) - # @color_map[absence] - if absence == UNPAID_ABSENCE - '#cc9557' - else - '#cc2767' + def color_for(absence) + # @color_map[absence] + if absence == UNPAID_ABSENCE + '#cc9557' + else + '#cc2767' + end end - end - def get_period_week(from) - get_period(from, from + 6) - end + def get_period_week(from) + get_period(from, from + 6) + end - def get_period_month(date) - get_set_cache(date.month) { Period.new(date.beginning_of_month, date.end_of_month) } - end + def get_period_month(date) + get_set_cache(date.month) { Period.new(date.beginning_of_month, date.end_of_month) } + end - def get_period(from, to) - get_set_cache([from, to]) { Period.new(from, to) } - end + def get_period(from, to) + get_set_cache([from, to]) { Period.new(from, to) } + end - def get_set_cache(key) - val = cache[key] - if val.nil? - val = yield - cache[key] = val + def get_set_cache(key) + val = cache[key] + if val.nil? + val = yield + cache[key] = val + end + val end - val - end - def cache - @cache ||= {} + def cache + @cache ||= {} + end end end diff --git a/app/domain/graphs/worktime_graph.rb b/app/domain/graphs/worktime_graph.rb index d5aa97c6b..57053e5d9 100644 --- a/app/domain/graphs/worktime_graph.rb +++ b/app/domain/graphs/worktime_graph.rb @@ -1,155 +1,161 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class WorktimeGraph - WORKTIME_ORDER = 'work_date, from_start_time, work_item_id, absence_id'.freeze - WORKTIME_CONDITIONS = ['(worktimes.report_type = ? OR worktimes.report_type = ?)', - StartStopType::INSTANCE.key, - HoursDayType::INSTANCE.key].freeze +module Graphs + class WorktimeGraph + WORKTIME_ORDER = 'work_date, from_start_time, work_item_id, absence_id' + WORKTIME_CONDITIONS = [ + '(worktimes.report_type = ? OR worktimes.report_type = ?)', + ReportType::StartStopType::INSTANCE.key, + ReportType::HoursDayType::INSTANCE.key + ].freeze - attr_reader :period, :employee + attr_reader :period, :employee - def initialize(period, employee) - @period = period.extend_to_weeks - @employee = employee + def initialize(period, employee) + @period = period.extend_to_weeks + @employee = employee - @work_items_eval = EmployeeWorkItemsEval.new(@employee.id) - @absences_eval = EmployeeAbsencesEval.new(@employee.id) + @work_items_eval = Evaluations::EmployeeWorkItemsEval.new(@employee.id) + @absences_eval = Evaluations::EmployeeAbsencesEval.new(@employee.id) - @color_map = AccountColorMapper.new - @weekly_boxes = {} - @monthly_boxes = {} - end + @color_map = Graphs::AccountColorMapper.new + @weekly_boxes = {} + @monthly_boxes = {} + end - def each_day - set_period_boxes(@monthly_boxes, Period.month_for(@period.start_date), HoursMonthType::INSTANCE) - @period.step do |day| - @current = Period.day_for(day) - compute_period_times day - yield day + def each_day + set_period_boxes(@monthly_boxes, Period.month_for(@period.start_date), ReportType::HoursMonthType::INSTANCE) + @period.step do |day| + @current = Period.day_for(day) + compute_period_times day + yield day + end end - end - def timeboxes - # must_hours are must_hours_per_day unless employment > 100% - must_hours = Holiday.musttime(@current.start_date) * must_hours_factor - period_boxes = concat_period_boxes - @total_hours = 0 - @boxes = [] - - # fill ordertimes - append_period_boxes period_boxes[:work_items], must_hours - append_account_boxes @work_items_eval.times(@current). - where(WORKTIME_CONDITIONS). - reorder(WORKTIME_ORDER). - includes(:work_item, :invoice) - - # add absencetimes, payed ones first - append_period_boxes period_boxes[:absences], must_hours - append_account_boxes(@absences_eval.times(@current). - joins('LEFT JOIN absences ON absences.id = absence_id'). - reorder('absences.payed DESC, work_date, from_start_time, absence_id'). - where(WORKTIME_CONDITIONS)) - - # add must_hours limit - insert_musthours_line must_hours - - @boxes - end + def timeboxes + # must_hours are must_hours_per_day unless employment > 100% + must_hours = Holiday.musttime(@current.start_date) * must_hours_factor + period_boxes = concat_period_boxes + @total_hours = 0 + @boxes = [] + + # fill ordertimes + append_period_boxes period_boxes[:work_items], must_hours + append_account_boxes @work_items_eval.times(@current) + .where(WORKTIME_CONDITIONS) + .reorder(WORKTIME_ORDER) + .includes(:work_item, :invoice) + + # add absencetimes, payed ones first + append_period_boxes period_boxes[:absences], must_hours + append_account_boxes(@absences_eval.times(@current) + .joins('LEFT JOIN absences ON absences.id = absence_id') + .reorder('absences.payed DESC, work_date, from_start_time, absence_id') + .where(WORKTIME_CONDITIONS)) + + # add must_hours limit + insert_musthours_line must_hours + + @boxes + end - def accounts?(type) - @color_map.accounts? type - end + def accounts?(type) + @color_map.accounts? type + end - def accounts_legend(type) - @color_map.accounts_legend type - end + def accounts_legend(type) + @color_map.accounts_legend type + end - def must_hours_factor - p = @current || @period - employment = @employee.employment_at(p.start_date) - employment ? [employment.percent_factor, 1.0].max : 1.0 - end + def must_hours_factor + p = @current || @period + employment = @employee.employment_at(p.start_date) + employment ? [employment.percent_factor, 1.0].max : 1.0 + end - private + private - def compute_period_times(day) - if day.wday == 1 - set_period_boxes(@weekly_boxes, Period.week_for(day), HoursWeekType::INSTANCE) - end - if day.mday == 1 - set_period_boxes(@monthly_boxes, Period.month_for(day), HoursMonthType::INSTANCE) + def compute_period_times(day) + set_period_boxes(@weekly_boxes, Period.week_for(day), ReportType::HoursWeekType::INSTANCE) if day.wday == 1 + return unless day.mday == 1 + + set_period_boxes(@monthly_boxes, Period.month_for(day), ReportType::HoursMonthType::INSTANCE) end - end - def set_period_boxes(hash, period, report_type) - hash[:work_items] = get_period_boxes(@work_items_eval, period, report_type) - hash[:absences] = get_period_boxes(@absences_eval, period, report_type) - end + def set_period_boxes(hash, period, report_type) + hash[:work_items] = get_period_boxes(@work_items_eval, period, report_type) + hash[:absences] = get_period_boxes(@absences_eval, period, report_type) + end - def get_period_boxes(evaluation, period, report_type) - work_items = evaluation.times(period). - where(report_type: report_type.key). - reorder(WORKTIME_ORDER) - # stretch by employment musttime if employment > 100% - hours = period.musttime.to_f * must_hours_factor - return [] if hours.zero? + def get_period_boxes(evaluation, period, report_type) + work_items = evaluation.times(period) + .where(report_type: report_type.key) + .reorder(WORKTIME_ORDER) + # stretch by employment musttime if employment > 100% + hours = period.musttime.to_f * must_hours_factor + return [] if hours.zero? - work_items.collect { |w| Timebox.new(w, color_for(w), Timebox.height_from_hours(w.hours / hours)) } - end + work_items.collect do |w| + Graphs::Timebox.new(w, color_for(w), Graphs::Timebox.height_from_hours(w.hours / hours)) + end + end - def concat_period_boxes - period_boxes = {} - @monthly_boxes.keys.each do |key| - period_boxes[key] = @monthly_boxes[key] + @weekly_boxes[key] + def concat_period_boxes + period_boxes = {} + @monthly_boxes.each_key do |key| + period_boxes[key] = @monthly_boxes[key] + @weekly_boxes[key] + end + period_boxes end - period_boxes - end - def append_period_boxes(period_boxes, must_hours) - period_boxes.each do |b| - box = b.clone - box.stretch(must_hours) - @boxes.push box - @total_hours += box.height / Timebox::PIXEL_PER_HOUR + def append_period_boxes(period_boxes, must_hours) + period_boxes.each do |b| + box = b.clone + box.stretch(must_hours) + @boxes.push box + @total_hours += box.height / Graphs::Timebox::PIXEL_PER_HOUR + end end - end - def append_account_boxes(worktimes) - worktimes.each do |w| - @boxes.push Timebox.new(w, color_for(w)) - @total_hours += w.hours + def append_account_boxes(worktimes) + worktimes.each do |w| + @boxes.push Graphs::Timebox.new(w, color_for(w)) + @total_hours += w.hours + end end - end - def insert_musthours_line(must_hours) - if @total_hours < must_hours - @boxes.push Timebox.blank(must_hours - @total_hours) - @boxes.push Timebox.must_hours(must_hours) - elsif @total_hours == must_hours - @boxes.push Timebox.must_hours(must_hours) - else - sum = 0 - limit = Timebox.height_from_hours(must_hours) - @boxes.each_index do |i| - sum += @boxes[i].height - diff = sum - limit - if diff > 0 - @boxes[i].height = @boxes[i].height - diff - @boxes.insert(i + 1, Timebox.must_hours(must_hours)) - @boxes.insert(i + 2, Timebox.new(@boxes[i].worktime, @boxes[i].color, diff, @boxes[i].tooltip)) - break - elsif diff.zero? - @boxes.insert(i + 1, Timebox.must_hours(must_hours)) - break + def insert_musthours_line(must_hours) + if @total_hours < must_hours + @boxes.push Graphs::Timebox.blank(must_hours - @total_hours) + @boxes.push Graphs::Timebox.must_hours(must_hours) + elsif @total_hours == must_hours + @boxes.push Graphs::Timebox.must_hours(must_hours) + else + sum = 0 + limit = Timebox.height_from_hours(must_hours) + @boxes.each_index do |i| + sum += @boxes[i].height + diff = sum - limit + if diff.positive? + @boxes[i].height = @boxes[i].height - diff + @boxes.insert(i + 1, Graphs::Timebox.must_hours(must_hours)) + @boxes.insert(i + 2, Graphs::Timebox.new(@boxes[i].worktime, @boxes[i].color, diff, @boxes[i].tooltip)) + break + elsif diff.zero? + @boxes.insert(i + 1, Graphs::Timebox.must_hours(must_hours)) + break + end end end end - end - def color_for(worktime) - @color_map[worktime.account] + def color_for(worktime) + @color_map[worktime.account] + end end end diff --git a/app/domain/invoicing.rb b/app/domain/invoicing.rb index 765b9ab44..b483b8874 100644 --- a/app/domain/invoicing.rb +++ b/app/domain/invoicing.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -8,7 +10,7 @@ module Invoicing cattr_accessor :instance def self.init - if Settings.small_invoice.client_id && Settings.small_invoice.client_secret && !Rails.env.test? + if Settings.small_invoice.api_token && !Rails.env.test? Invoicing.instance = Invoicing::SmallInvoice::Interface.new InvoicingSyncJob.schedule if Delayed::Job.table_exists? end diff --git a/app/domain/invoicing/error.rb b/app/domain/invoicing/error.rb index 99dc69c71..60b7e05e9 100644 --- a/app/domain/invoicing/error.rb +++ b/app/domain/invoicing/error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/domain/invoicing/interface.rb b/app/domain/invoicing/interface.rb index 7a90150bf..afd41be0f 100644 --- a/app/domain/invoicing/interface.rb +++ b/app/domain/invoicing/interface.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -6,20 +8,16 @@ module Invoicing class Interface # Stores a new or an existing invoice with the given positions in the remote system. - def save_invoice(_invoice, _positions) - end + def save_invoice(_invoice, _positions); end # Fetches and updates the data for the given invoice from the remote system. # The invoice will be destroyed if the remote system deleted it. - def sync_invoice(_invoice) - end + def sync_invoice(_invoice); end # Delete a given invoice in the remote system. - def delete_invoice(_invoice) - end + def delete_invoice(_invoice); end # Sync all entities to the invoicing system, overriding remote changes. - def sync_all - end + def sync_all; end end end diff --git a/app/domain/invoicing/position.rb b/app/domain/invoicing/position.rb index 8a175717c..fb164a380 100644 --- a/app/domain/invoicing/position.rb +++ b/app/domain/invoicing/position.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/domain/invoicing/small_invoice.rb b/app/domain/invoicing/small_invoice.rb index cd4ec861a..c96e78177 100644 --- a/app/domain/invoicing/small_invoice.rb +++ b/app/domain/invoicing/small_invoice.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/domain/invoicing/small_invoice/address_sync.rb b/app/domain/invoicing/small_invoice/address_sync.rb deleted file mode 100644 index 0d160d182..000000000 --- a/app/domain/invoicing/small_invoice/address_sync.rb +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -module Invoicing - module SmallInvoice - # One-way sync from PuzzleTime to Small Invoice for clients, contacts and billing addresses. - class AddressSync - class << self - def notify_sync_error(error, address = nil) - parameters = address.present? ? record_to_params(address) : {} - parameters[:code] = error.code if error.respond_to?(:code) - parameters[:data] = error.data if error.respond_to?(:data) - Airbrake.notify(error, parameters) if airbrake? - Raven.capture_exception(error, extra: parameters) if sentry? - end - - private - - def airbrake? - ENV['RAILS_AIRBRAKE_HOST'].present? - end - - def sentry? - ENV['SENTRY_DSN'].present? - end - - def record_to_params(record, prefix = 'billing_address') - { - "#{prefix}_id" => record.id, - "#{prefix}_invoicing_key" => record.invoicing_key, - "#{prefix}_shortname" => record.try(:shortname), - "#{prefix}_label" => record.try(:label) || record.to_s, - "#{prefix}_errors" => record.errors.messages, - "#{prefix}_changes" => record.changes - } - end - end - - delegate :notify_sync_error, to: 'self.class' - attr_reader :client, :remote_keys - - class_attribute :rate_limiter - self.rate_limiter = RateLimiter.new(Settings.small_invoice.request_rate) - - def initialize(client, remote_keys = nil) - @client = client - @remote_keys = remote_keys || fetch_remote_keys - end - - def sync - failed = [] - ::BillingAddress.includes(:client).where(client_id: client.id).find_each do |billing_address| - key(billing_address) ? update_remote(billing_address) : create_remote(billing_address) - rescue StandardError => e - failed << billing_address.id - Rails.logger.error e.message - Rails.logger.error e.backtrace - notify_sync_error(e, billing_address) - end - Rails.logger.error "Failed Address Syncs: #{failed.inspect}" if failed.any? - end - - private - - def fetch_remote_keys - api.list(Entity::Address.path(client)).map do |address| - address['id'] - end - end - - def update_remote(address) - rate_limiter.run { api.edit(Entity::Address.new(address).path, data(address)) } - end - - def create_remote(address) - response = rate_limiter.run { api.add(Entity::Address.path(client), data(address)) } - address.update_column(:invoicing_key, response.fetch('id')) - end - - def data(address) - Entity::Address.new(address).to_hash - end - - def reset_invoicing_keys(address, invoicing_key = nil) - address.update_column(:invoicing_key, invoicing_key) - end - - def key(address) - address.invoicing_key if key_exists_remotely?(address) - end - - def key_exists_remotely?(address) - address.invoicing_key.present? && remote_keys.map(&:to_s).include?(address.invoicing_key) - end - - def api - Invoicing::SmallInvoice::Api.instance - end - end - end -end diff --git a/app/domain/invoicing/small_invoice/api.rb b/app/domain/invoicing/small_invoice/api.rb index 645b1b883..92c85aa9d 100644 --- a/app/domain/invoicing/small_invoice/api.rb +++ b/app/domain/invoicing/small_invoice/api.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -8,121 +10,58 @@ module SmallInvoice class Api include Singleton + ENDPOINTS = %w(invoice invoice/pdf client).freeze HTTP_TIMEOUT = 300 # seconds - LIST_PAGES_LIMIT = 100 - LIST_ENTRIES = 200 # the v2 api allows max 200 entries per page - - def list(path, **params) - # The v2 api returns max 200 entries per query, so we loop through all pages and collect the result. - (0..LIST_PAGES_LIMIT).each_with_object([]) do |index, result| - response = get_json(path, **params.reverse_merge(limit: LIST_ENTRIES, offset: LIST_ENTRIES * index)) - result.append(*response['items']) - return result unless response.dig('pagination', 'next') - end + def list(endpoint) + response = get_json(endpoint, :list) + response['items'] end - def get(path, **params) - response = get_json(path, **params) - response.fetch('item') + def get(endpoint, id) + response = get_json(endpoint, :get, id: id) + response['item'] end - def add(path, data) - response = post_json(path, **data) - response.fetch('item') + def add(endpoint, data) + response = post_request(endpoint, :add, data) + response['id'] end - def edit(path, data) - put_json(path, **data) + def edit(endpoint, id, data) + post_request(endpoint, :edit, data, id: id) nil end - def delete(path) - delete_request(path) + def delete(endpoint, id) + post_request(endpoint, :delete, nil, id: id) nil end - def get_raw(path, auth: true, **params) - get_request(path, auth: auth, **params).body + def get_raw(endpoint, action, id) + get_request(endpoint, action, id: id).body end private - def access_token - # fetch a new token if we have none yet or if the existing one is expired - @access_token, @expires_at = get_access_token unless @expires_at&.>(Time.zone.now) - @access_token - end - - # Get a new access token from the smallinvoice api. - # Returns an array with the access_token and the expiration time of this token. - def get_access_token - timestamp = Time.zone.now - - response = post_json( - 'auth/access-tokens', - auth: false, - grant_type: 'client_credentials', - client_id: settings.client_id, - client_secret: settings.client_secret, - scope: 'invoice contact' - ) - - response.fetch_values('access_token', 'expires_in').yield_self do |token, expires_in| - [token, timestamp + expires_in] - end - end - - def get_json(path, auth: true, **params) - response = get_request(path, auth: auth, **params) - handle_json_response(response) - end - - def get_request(path, auth: true, **params) - url = build_url(path, **params) - request = Net::HTTP::Get.new(url.request_uri) - request['Authorization'] = "Bearer #{access_token}" if auth - - http(url).request(request) - end - - def post_json(path, auth: true, **payload) - response = post_request(path, payload.to_json, auth: auth) - handle_json_response(response) - end - - def post_request(path, data, auth: true) - url = build_url(path) - request = Net::HTTP::Post.new(url, - 'Content-Type' => 'application/json') - request['Authorization'] = "Bearer #{access_token}" if auth - request.body = data - - http(url).request(request) - end - - def put_json(path, auth: true, **payload) - response = put_request(path, payload.to_json, auth: auth) + def get_json(endpoint, action, **params) + response = get_request(endpoint, action, **params) handle_json_response(response) end - def put_request(path, data, auth: true) - url = build_url(path) - request = Net::HTTP::Put.new(url, - 'Content-Type' => 'application/json') - request['Authorization'] = "Bearer #{access_token}" if auth - request.body = data - + def get_request(endpoint, action, **params) + url = uri(endpoint, action, **params) + request = Net::HTTP::Get.new(url.path) http(url).request(request) end - def delete_request(path, auth: true) - url = build_url(path) - request = Net::HTTP::Delete.new(url, - 'Content-Type' => 'application/json') - request['Authorization'] = "Bearer #{access_token}" if auth + def post_request(endpoint, action, data, **params) + url = uri(endpoint, action, **params) + request = Net::HTTP::Post.new(url.path) + request.set_form_data(data ? { data: data.to_json } : {}) - http(url).request(request) + response = http(url).request(request) + handle_json_response(response, data) end def http(url) @@ -132,34 +71,25 @@ def http(url) end end - def build_url(path, **params) - url = [settings.url, path].join('/') - URI.parse(url).tap do |url| - url.query = URI.encode_www_form(params) if params.present? - end - end + def uri(endpoint, action, **params) + fail(ArgumentError, "Unknown endpoint #{endpoint}") unless ENDPOINTS.include?(endpoint.to_s) - def handle_json_response(response) - handle_error(response) unless response.is_a? Net::HTTPSuccess - - return {} if response.body.blank? - - parse_json_response(response) + params[:token] = Settings.small_invoice.api_token + args = params.collect { |k, v| "#{k}/#{v}" }.join('/') + URI("#{Settings.small_invoice.url}/#{endpoint}/#{action}/#{args}") end - def handle_error(response) - payload = parse_json_response(response) - fail Invoicing::Error.new(response.message, response.code, payload) - end + def handle_json_response(response, data = nil) + return {} if response.body.blank? - def parse_json_response(response) - JSON.parse(response.body) + json = JSON.parse(response.body) + if json['error'] + fail Invoicing::Error.new(json['errormessage'], json['errorcode'], data) + else + json + end rescue JSON::ParserError - fail Invoicing::Error.new('JSON::ParserError', response.code, response.body) - end - - def settings - Settings.small_invoice + fail Invoicing::Error.new(response.body, response.code, data) end end end diff --git a/app/domain/invoicing/small_invoice/client_sync.rb b/app/domain/invoicing/small_invoice/client_sync.rb index 11138e8ab..daaa325ae 100644 --- a/app/domain/invoicing/small_invoice/client_sync.rb +++ b/app/domain/invoicing/small_invoice/client_sync.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -10,23 +12,19 @@ class ClientSync class << self def perform remote_keys = fetch_remote_keys - failed = [] ::Client.includes(:work_item, :contacts, :billing_addresses).find_each do |client| if client.billing_addresses.present? # required by small invoice begin new(client, remote_keys).sync - rescue => error - failed << client.id - notify_sync_error(error, client) + rescue StandardError => e + notify_sync_error(e, client) end end end - Rails.logger.error "Failed Client Syncs: #{failed.inspect}" if failed.any? end def fetch_remote_keys - path = Invoicing::SmallInvoice::Entity::Contact.path - Invoicing::SmallInvoice::Api.instance.list(path).each_with_object({}) do |client, hash| + SmallInvoice::Api.instance.list(:client).each_with_object({}) do |client, hash| hash[client['number']] = client['id'] end end @@ -51,19 +49,18 @@ def sentry? def record_to_params(record, prefix = 'client') { - "#{prefix}_id" => record.id, + "#{prefix}_id" => record.id, "#{prefix}_invoicing_key" => record.invoicing_key, - "#{prefix}_shortname" => record.try(:shortname), - "#{prefix}_label" => record.try(:label) || record.to_s, - "#{prefix}_errors" => record.errors.messages, - "#{prefix}_changes" => record.changes + "#{prefix}_shortname" => record.try(:shortname), + "#{prefix}_label" => record.try(:label) || record.to_s, + "#{prefix}_errors" => record.errors.messages, + "#{prefix}_changes" => record.changes } end end delegate :notify_sync_error, to: 'self.class' attr_reader :client, :remote_keys - class_attribute :rate_limiter self.rate_limiter = RateLimiter.new(Settings.small_invoice.request_rate) @@ -73,14 +70,32 @@ def initialize(client, remote_keys = nil) end def sync - key ? update_remote : create_remote - - ContactSync.new(client).sync - AddressSync.new(client).sync + if key + update_remote_with_timeouts + else + create_remote + set_association_keys_from_remote + end end private + def update_remote_with_timeouts + begin + update_remote + set_association_keys_from_remote + rescue Invoicing::Error => e + if e.code.to_s == '504' + # request is supposed to terminate eventually in the case of a gateway timeout, + # so schedule #set_association_keys for later + delay(run_at: 30.minutes.from_now).set_association_keys_from_remote + nil + else + raise + end + end + end + def update_remote if client.invoicing_key != key # Conflicting datasets in ptime <=> smallinvoice. We need to update the invoicing_key @@ -88,7 +103,7 @@ def update_remote # otherwise sync will abort because of conflicts. reset_invoicing_keys(key) end - rate_limiter.run { api.edit(Entity::Contact.new(client).path, data) } + rate_limiter.run { api.edit(:client, key, data) } end def create_remote @@ -97,23 +112,56 @@ def create_remote # executing the add action to avoid 15016 "no rights / not found" errors. reset_invoicing_keys - response = rate_limiter.run { api.add(Entity::Contact.path, data) } - client.update_column(:invoicing_key, response.fetch('id')) - client.billing_addresses.first.update_column(:invoicing_key, response.fetch('main_address_id')) + key = rate_limiter.run { api.add(:client, data) } + client.update_column(:invoicing_key, key) end def data - Entity::Contact.new(client).to_hash + Invoicing::SmallInvoice::Entity::Client.new(client).to_hash + end + + def set_association_keys_from_remote + remote = fetch_remote(client.invoicing_key) + set_association_keys(remote) if remote + nil end def fetch_remote(key) - rate_limiter.run { api.get(Entity::Contact.path(invoicing_key: key)) } + rate_limiter.run { api.get(:client, key) } rescue Invoicing::Error => e - if e.message == 'No Objects or too many found' - reset_invoicing_keys - nil - else - raise + raise unless e.message == 'No Objects or too many found' + + reset_invoicing_keys + nil + end + + def set_association_keys(remote) + set_association_key(Invoicing::SmallInvoice::Entity::Address, + client.billing_addresses, + remote.fetch('addresses', [])) + set_association_key(Invoicing::SmallInvoice::Entity::Contact, + client.contacts, + remote.fetch('contacts', [])) + end + + def set_association_key(entity, list, remote_list) + list.each do |item| + local_item = entity.new(item) + remote_keys = remote_list.select { |h| local_item == h }.map { |h| h['id'].to_s.presence }.compact + next if remote_keys.blank? || remote_keys.include?(item.invoicing_key) + + local_keys = list.model.where(invoicing_key: remote_keys).pluck(:invoicing_key) + new_remote_keys = remote_keys - local_keys + if new_remote_keys.blank? + notify_sync_error(Invoicing::Error.new('Unable to sync from remote, ' \ + 'record with invoicing_key already exists', + nil, + local_item: item.id, + invoicing_keys: remote_keys, + type: entity.name)) + else + item.update_column(:invoicing_key, new_remote_keys.first) + end end end @@ -137,7 +185,7 @@ def key_exists_remotely? end def api - Invoicing::SmallInvoice::Api.instance + Api.instance end end end diff --git a/app/domain/invoicing/small_invoice/contact_sync.rb b/app/domain/invoicing/small_invoice/contact_sync.rb deleted file mode 100644 index d8020c10b..000000000 --- a/app/domain/invoicing/small_invoice/contact_sync.rb +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -module Invoicing - module SmallInvoice - # One-way sync from PuzzleTime to Small Invoice for clients, contacts and billing addresses. - class ContactSync - class << self - def notify_sync_error(error, contact = nil) - parameters = contact.present? ? record_to_params(contact) : {} - parameters[:code] = error.code if error.respond_to?(:code) - parameters[:data] = error.data if error.respond_to?(:data) - Airbrake.notify(error, parameters) if airbrake? - Raven.capture_exception(error, extra: parameters) if sentry? - end - - private - - def airbrake? - ENV['RAILS_AIRBRAKE_HOST'].present? - end - - def sentry? - ENV['SENTRY_DSN'].present? - end - - def record_to_params(record, prefix = 'billing_address') - { - "#{prefix}_id" => record.id, - "#{prefix}_invoicing_key" => record.invoicing_key, - "#{prefix}_shortname" => record.try(:shortname), - "#{prefix}_label" => record.try(:label) || record.to_s, - "#{prefix}_errors" => record.errors.messages, - "#{prefix}_changes" => record.changes - } - end - end - - delegate :notify_sync_error, to: 'self.class' - attr_reader :client, :remote_keys - - class_attribute :rate_limiter - self.rate_limiter = RateLimiter.new(Settings.small_invoice.request_rate) - - def initialize(client, remote_keys = nil) - @client = client - @remote_keys = remote_keys || fetch_remote_keys - end - - def sync - failed = [] - ::Contact.includes(:client).where(client_id: client.id).find_each do |contact| - key(contact) ? update_remote(contact) : create_remote(contact) - rescue StandardError => e - failed << contact.id - notify_sync_error(e, contact) - end - Rails.logger.error "Failed Contact Syncs: #{failed.inspect}" if failed.any? - end - - private - - def fetch_remote_keys - api.list(Entity::Person.path(client)).map do |person| - person['id'] - end - end - - def update_remote(contact) - if contact.invoicing_key != key(contact) - # Conflicting datasets in ptime <=> smallinvoice. We need to update the invoicing_key - # of the client in ptime and clear the invoicing_keys of the addresses and contacts, - # otherwise sync will abort because of conflicts. - reset_invoicing_keys(contact, key(contact)) - end - rate_limiter.run { api.edit(Entity::Person.new(contact).path, data(contact)) } - end - - def create_remote(contact) - # Local clients may have an invoice key that does't exist in smallinvoice (e.g. when - # using a productive dump on ptime integration). So reset the invoicing keys before - # executing the add action to avoid 15016 "no rights / not found" errors. - reset_invoicing_keys(contact) - response = rate_limiter.run { api.add(Entity::Person.path(client), data(contact)) } - contact.update_column(:invoicing_key, response.fetch('id')) - end - - def data(contact) - Entity::Person.new(contact).to_hash - end - - def reset_invoicing_keys(contact, invoicing_key = nil) - contact.update_column(:invoicing_key, invoicing_key) - end - - def key(contact) - contact.invoicing_key if key_exists_remotely?(contact) - end - - def key_exists_remotely?(contact) - contact.invoicing_key.present? && remote_keys.map(&:to_s).include?(contact.invoicing_key) - end - - def api - Invoicing::SmallInvoice::Api.instance - end - end - end -end diff --git a/app/domain/invoicing/small_invoice/entity.rb b/app/domain/invoicing/small_invoice/entity.rb deleted file mode 100644 index 837e8aa36..000000000 --- a/app/domain/invoicing/small_invoice/entity.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Invoicing - module SmallInvoice - module Entity; end - end -end \ No newline at end of file diff --git a/app/domain/invoicing/small_invoice/entity/address.rb b/app/domain/invoicing/small_invoice/entity/address.rb index 0fdf3b409..520bb9654 100644 --- a/app/domain/invoicing/small_invoice/entity/address.rb +++ b/app/domain/invoicing/small_invoice/entity/address.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -7,20 +9,10 @@ module Invoicing module SmallInvoice module Entity class Address < Base - ENDPOINT = 'addresses'.freeze - - def self.path(client, invoicing_key: nil) - [*Entity::Contact.new(client).path, ENDPOINT, invoicing_key].compact if client.persisted? - end - - def path - self.class.path(entry.client, invoicing_key: entry.invoicing_key) if persisted? - end - def to_hash street, street2 = entry.supplement? ? [entry.supplement, entry.street] : [entry.street, nil] - with_id(street: street, - street2: street2, + with_id(street:, + street2:, postcode: entry.zip_code, city: entry.town, country: entry.country) diff --git a/app/domain/invoicing/small_invoice/entity/base.rb b/app/domain/invoicing/small_invoice/entity/base.rb index be3848174..a2d6a9c09 100644 --- a/app/domain/invoicing/small_invoice/entity/base.rb +++ b/app/domain/invoicing/small_invoice/entity/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -39,11 +41,7 @@ def stringify(hash) memo[key.to_s] = value.to_s.strip end end - - def persisted? - entry.invoicing_key.present? - end end end end -end \ No newline at end of file +end diff --git a/app/domain/invoicing/small_invoice/entity/client.rb b/app/domain/invoicing/small_invoice/entity/client.rb new file mode 100644 index 000000000..6e89fe321 --- /dev/null +++ b/app/domain/invoicing/small_invoice/entity/client.rb @@ -0,0 +1,34 @@ +# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of +# PuzzleTime and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/puzzle/puzzletime. + +module Invoicing + module SmallInvoice + module Entity + class Client < Base + def to_hash + { + number: entry.shortname, + name: entry.name, + type: constant(:client_type), + language: constant(:language), + einvoice_account_id: entry.e_bill_account_key, + addresses: entry.billing_addresses.list.collect.with_index do |a, i| + Address.new(a).to_hash.update(primary: i.zero?) + end, + contacts: entry.contacts.list.collect.with_index do |c, i| + # Set primary on the first contact to ensure we always have a + # primary contact for this client. + # Clients can only have one primary contact, + # setting more than one contact to primary will overwrite the + # existing one. + # See #20498 + Entity::Contact.new(c).to_hash.update(primary: i.zero?) + end + } + end + end + end + end +end diff --git a/app/domain/invoicing/small_invoice/entity/contact.rb b/app/domain/invoicing/small_invoice/entity/contact.rb index e6a39c549..dccec7b3d 100644 --- a/app/domain/invoicing/small_invoice/entity/contact.rb +++ b/app/domain/invoicing/small_invoice/entity/contact.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -7,27 +9,12 @@ module Invoicing module SmallInvoice module Entity class Contact < Base - ENDPOINT = 'contacts'.freeze - - def self.path(invoicing_key: nil) - [ENDPOINT, invoicing_key].compact - end - - def path - self.class.path(invoicing_key: entry.invoicing_key) - end - def to_hash - { - number: entry.shortname, - relation: ['CL'], # TODO: move to config/settings.yml:small_invoice/constants - type: 'C', # TODO: move to config/settings.yml:small_invoice/constants - name: entry.name, - communication_language: constant(:language), - ebill_account_id: entry.e_bill_account_key, - - main_address: Entity::Address.new(entry.billing_addresses.first).to_hash - } + with_id(surname: entry.lastname, + name: entry.firstname, + email: entry.email, + phone: entry.phone, + gender: constant(:gender_id)) end end end diff --git a/app/domain/invoicing/small_invoice/entity/invoice.rb b/app/domain/invoicing/small_invoice/entity/invoice.rb index c76ec49ed..cb759250e 100644 --- a/app/domain/invoicing/small_invoice/entity/invoice.rb +++ b/app/domain/invoicing/small_invoice/entity/invoice.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -7,8 +9,6 @@ module Invoicing module SmallInvoice module Entity class Invoice < Base - ENDPOINT = ['receivables', 'invoices'].freeze - attr_reader :positions def initialize(invoice, positions) @@ -16,45 +16,32 @@ def initialize(invoice, positions) @positions = positions end - def self.path(invoicing_key: nil) - [*ENDPOINT, invoicing_key].compact - end - - def path - self.class.path(invoicing_key: entry.invoicing_key) - end - - def pdf_path - [*path, 'pdf'] - end - def to_hash { number: entry.reference, - contact_id: Integer(entry.billing_address.client.invoicing_key), - contact_address_id: Integer(entry.billing_address.invoicing_key), - contact_person_id: entry.billing_address.contact.try(:invoicing_key)&.to_i, + client_id: entry.billing_address.client.invoicing_key, + client_address_id: entry.billing_address.invoicing_key, + client_contact_id: entry.billing_address.contact.try(:invoicing_key), + currency: Settings.defaults.currency, + title: entry.title, + period: entry.period.to_s, date: entry.billing_date, due: entry.due_date, - period: entry.period.to_s, - currency: Settings.defaults.currency, - vat_included: constant(:vat_included), + account_id: constant(:account_id), + esr: bool_constant(:esr), + esr_singlepage: bool_constant(:esr_singlepage), + lsvplus: bool_constant(:lsvplus), + dd: bool_constant(:debit_direct), + conditions: conditions, + introduction: introduction, language: constant(:language), - - positions: positions.collect do |p| - Entity::Position.new(p).to_hash - end, - - texts: [ - { - status: 'D', # TODO: do we need other states? - title: entry.title, - conditions: conditions, - introduction: introduction - } - ] - - # totalamount: entry.total_amount.round(2), + paypal: bool_constant(:paypal), + paypal_url: constant(:paypay_url), + vat_included: constant(:vat_included), + totalamount: entry.total_amount.round(2), + positions: positions.collect do |p| + Invoicing::SmallInvoice::Entity::Position.new(p).to_hash + end } end @@ -66,9 +53,7 @@ def conditions def introduction string = 'Besten Dank für Ihren Auftrag' - if entry.contract_reference.present? - string += "\n\nIhre Referenzinformationen:\n#{entry.contract_reference}" - end + string += "\n\nIhre Referenzinformationen:\n#{entry.contract_reference}" if entry.contract_reference.present? string end end diff --git a/app/domain/invoicing/small_invoice/entity/person.rb b/app/domain/invoicing/small_invoice/entity/person.rb deleted file mode 100644 index a0fcecdf1..000000000 --- a/app/domain/invoicing/small_invoice/entity/person.rb +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -module Invoicing - module SmallInvoice - module Entity - class Person < Base - ENDPOINT = 'people'.freeze - - def self.path(client, invoicing_key: nil) - [*Entity::Contact.new(client).path, ENDPOINT, invoicing_key].compact if client.persisted? - end - - def path - self.class.path(entry.client, invoicing_key: entry.invoicing_key) if persisted? - end - - def to_hash - with_id(surname: entry.lastname, - name: entry.firstname, - email: entry.email, - phone: entry.phone, - gender: 'F') - end - end - end - end -end diff --git a/app/domain/invoicing/small_invoice/entity/position.rb b/app/domain/invoicing/small_invoice/entity/position.rb index 1679b0305..d0c1427a8 100644 --- a/app/domain/invoicing/small_invoice/entity/position.rb +++ b/app/domain/invoicing/small_invoice/entity/position.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -9,15 +11,14 @@ module Entity class Position < Base def to_hash { - type: 'N', - catalog_type: constant(:position_type), + type: constant(:position_type_id), number: nil, name: entry.name, description: nil, - price: post.offered_rate.round(2).to_f, - vat: Settings.defaults.vat, - amount: entry.total_hours.round(2).to_f, - unit_id: constant(:unit_id) + cost: post.offered_rate.try(:round, 2), + unit: constant(:unit_id), + amount: entry.total_hours.round(2), + vat: Settings.defaults.vat } end diff --git a/app/domain/invoicing/small_invoice/interface.rb b/app/domain/invoicing/small_invoice/interface.rb index 7a539e088..559fb1c34 100644 --- a/app/domain/invoicing/small_invoice/interface.rb +++ b/app/domain/invoicing/small_invoice/interface.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -19,7 +21,7 @@ def sync_invoice(invoice) def delete_invoice(invoice) return unless invoice.invoicing_key? - Invoicing::SmallInvoice::Api.instance.delete(Entity::Invoice.path(invoicing_key: invoice.invoicing_key)) + Api.instance.delete(:invoice, invoice.invoicing_key) end def sync_all @@ -28,7 +30,7 @@ def sync_all end def get_pdf(invoice) - Invoicing::SmallInvoice::Api.instance.get_raw(Entity::Invoice.new(invoice, nil).pdf_path) + Api.instance.get_raw('invoice', :pdf, invoice.invoicing_key) end end end diff --git a/app/domain/invoicing/small_invoice/invoice_store.rb b/app/domain/invoicing/small_invoice/invoice_store.rb index 7bcaaedae..0aed3bab9 100644 --- a/app/domain/invoicing/small_invoice/invoice_store.rb +++ b/app/domain/invoicing/small_invoice/invoice_store.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -17,13 +19,12 @@ def initialize(invoice) def save(positions) assert_remote_client_exists - entity = Entity::Invoice.new(invoice, positions) + data = Invoicing::SmallInvoice::Entity::Invoice.new(invoice, positions).to_hash if invoice.invoicing_key? - api.edit(entity.path, entity.to_hash) + api.edit(:invoice, invoice.invoicing_key, data) invoice.invoicing_key else - result = api.add(entity.class.path, entity.to_hash) - result['id'] + api.add(:invoice, data) end end @@ -41,7 +42,7 @@ def assert_remote_client_exists end def api - Invoicing::SmallInvoice::Api.instance + Api.instance end end end diff --git a/app/domain/invoicing/small_invoice/invoice_sync.rb b/app/domain/invoicing/small_invoice/invoice_sync.rb index 0ef70b884..ca189c0c0 100644 --- a/app/domain/invoicing/small_invoice/invoice_sync.rb +++ b/app/domain/invoicing/small_invoice/invoice_sync.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -7,40 +9,28 @@ module Invoicing module SmallInvoice # One-way sync of invoices from Small Invoice to PuzzleTime class InvoiceSync - # status (string): status of invoice, possible values: - # DR - draft, S - sent, P - paid, PP - partially paid, R1 - 1st reminder, R2 - 2nd reminder, R3 - 3rd reminder, - # R - reminder, DC - debt collection, C - cancelled, D - deleted (but still visible) , - STATUS = { - 'DR' => 'draft', - 'S' => 'sent', - 'P' => 'paid', - 'PP' => 'partially_paid', - 'R1' => 'sent', - 'R2' => 'sent', - 'R3' => 'sent', - 'R' => 'sent', - 'DC' => 'dept_collection', - 'C' => 'cancelled', - 'D' => 'deleted' - }.freeze + STATUS = { 1 => 'sent', # sent / open + 2 => 'paid', # paid + 3 => 'sent', # 1st reminder + 4 => 'sent', # 2nd reminder + 5 => 'sent', # 3rd reminder + 6 => 'cancelled', # cancelled + 7 => 'draft', # draft + 11 => 'partially_paid', # partially paid + 12 => 'sent', # reminder + 99 => 'deleted' }.freeze # deleted attr_reader :invoice - class_attribute :rate_limiter self.rate_limiter = RateLimiter.new(Settings.small_invoice.request_rate) class << self def sync_unpaid - failed = [] unpaid_invoices.find_each do |invoice| - begin - new(invoice).sync - rescue => error - failed << invoice.id - notify_sync_error(error, invoice) - end + new(invoice).sync + rescue StandardError => e + notify_sync_error(e, invoice) end - Rails.logger.error "Failed Invoice Syncs: #{failed.inspect}" if failed.any? end private @@ -48,7 +38,9 @@ def sync_unpaid def unpaid_invoices Invoice .joins(order: :status) - .where.not(status: 'paid', invoicing_key: nil, order_statuses: { closed: true }) + .where.not(status: :paid) + .where.not(invoicing_key: nil) + .where.not(order_statuses: { closed: true }) end def notify_sync_error(error, invoice) @@ -69,11 +61,11 @@ def sentry? def record_to_params(record, prefix = 'invoice') { - "#{prefix}_id" => record.id, + "#{prefix}_id" => record.id, "#{prefix}_invoicing_key" => record.invoicing_key, - "#{prefix}_label" => record.try(:label) || record.to_s, - "#{prefix}_errors" => record.errors.messages, - "#{prefix}_changes" => record.changes + "#{prefix}_label" => record.try(:label) || record.to_s, + "#{prefix}_errors" => record.errors.messages, + "#{prefix}_changes" => record.changes } end end @@ -84,9 +76,7 @@ def initialize(invoice) # Fetch an invoice from remote and update the local values def sync - return unless invoice.invoicing_key - - item = rate_limiter.run { api.get(Entity::Invoice.path(invoicing_key: invoice.invoicing_key), with: 'positions') } + item = rate_limiter.run { api.get(:invoice, invoice.invoicing_key) } sync_remote(item) rescue Invoicing::Error => e if e.code == 15_016 # no rights / not found @@ -126,8 +116,8 @@ def delete_invoice(force = false) def total_hours(item) item['positions'].select do |p| - p['catalog_type'] == Settings.small_invoice.constants.position_type && - p['unit_id'] == Settings.small_invoice.constants.unit_id + p['type'] == Settings.small_invoice.constants.position_type_id && + p['unit'] == Settings.small_invoice.constants.unit_id end.collect do |p| p['amount'] end.sum @@ -136,12 +126,13 @@ def total_hours(item) # item['totalamount'] always includes vat # item['vat_included'] tells whether position totals already include vat or not. def total_amount_without_vat(item) - item['positions'].select { |p| p['price'] }.map do |p| - total = p['price'] * p['amount'] + vat_included = !item['vat_included'].zero? + item['positions'].select { |p| p['cost'] }.map do |p| + total = p['cost'] * p['amount'] total -= position_discount(p, total) - total -= position_included_vat(p, total) if item['vat_included'] + total -= position_included_vat(p, total) if vat_included total - end.sum + end end def position_discount(p, total) @@ -161,7 +152,7 @@ def position_included_vat(p, total) end def api - Invoicing::SmallInvoice::Api.instance + Api.instance end end end diff --git a/app/domain/order/cockpit.rb b/app/domain/order/cockpit.rb index 1695fcbb3..e929d8d65 100644 --- a/app/domain/order/cockpit.rb +++ b/app/domain/order/cockpit.rb @@ -1,77 +1,81 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Order::Cockpit - attr_reader :order, :rows +class Order + class Cockpit + attr_reader :order, :rows - EM_DASH = '―'.freeze + EM_DASH = '―' - def initialize(order) - @order = order - @rows = build_rows - end + def initialize(order) + @order = order + @rows = build_rows + end - def billed_amount - @billed_amount ||= order.invoices.where.not(status: 'cancelled').sum(:total_amount).to_f - end + def billed_amount + @billed_amount ||= order.invoices.where.not(status: 'cancelled').sum(:total_amount).to_f + end - def budget_open - total.cells[:budget].amount.to_f - billed_amount - end + def budget_open + total.cells[:budget].amount.to_f - billed_amount + end - # Ist-R[currency] / Ist-V[h] - def billed_rate - billable_hours > 0 ? billed_amount / billable_hours : nil - end + # Ist-R[currency] / Ist-V[h] + def billed_rate + billable_hours.positive? ? billed_amount / billable_hours : nil + end - # Ist-R[h] / Ist[h] x 100 - def cost_effectiveness_current - result = (order.invoices.where.not(status: 'cancelled').sum(:total_hours).to_f / total_hours) * 100.0 - result.finite? ? result.round : EM_DASH - end + # Ist-R[h] / Ist[h] x 100 + def cost_effectiveness_current + result = (order.invoices.where.not(status: 'cancelled').sum(:total_hours).to_f / total_hours) * 100.0 + result.finite? ? result.round : EM_DASH + end - # (Ist[h]-Ist-NV[h]) / Ist[h] x 100 - def cost_effectiveness_forecast - result = (1 - not_billable_hours / total_hours) * 100.0 - result.finite? ? result.round : EM_DASH - end + # (Ist[h]-Ist-NV[h]) / Ist[h] x 100 + def cost_effectiveness_forecast + result = (1 - (not_billable_hours / total_hours)) * 100.0 + result.finite? ? result.round : EM_DASH + end - def accounting_posts - @accounting_posts ||= order.accounting_posts.includes(:portfolio_item).list.to_a - end + def accounting_posts + @accounting_posts ||= order.accounting_posts.includes(:portfolio_item).list.to_a + end - private + private - def not_billable_hours - total.cells[:not_billable].hours.to_f - end + def not_billable_hours + total.cells[:not_billable].hours.to_f + end - def billable_hours - total_hours - not_billable_hours - end + def billable_hours + total_hours - not_billable_hours + end - def total_hours - total.cells[:supplied_services].hours.to_f - end + def total_hours + total.cells[:supplied_services].hours.to_f + end - def build_rows - if sub_levels? - rows = accounting_posts.collect { |p| AccountingPostRow.new(p) } - total = TotalRow.new(rows) - [total, *rows] - else - [AccountingPostRow.new(accounting_posts.first, order.work_item.path_shortnames)] + def build_rows + if sub_levels? + rows = accounting_posts.collect { |p| AccountingPostRow.new(p) } + total = TotalRow.new(rows) + [total, *rows] + else + [AccountingPostRow.new(accounting_posts.first, order.work_item.path_shortnames)] + end end - end - def total - rows.first - end + def total + rows.first + end - def sub_levels? - accounting_posts.size != 1 || - accounting_posts.first.work_item_id != order.work_item_id + def sub_levels? + accounting_posts.size != 1 || + accounting_posts.first.work_item_id != order.work_item_id + end end end diff --git a/app/domain/order/cockpit/accounting_post_row.rb b/app/domain/order/cockpit/accounting_post_row.rb index ca1d70e58..4153b840a 100644 --- a/app/domain/order/cockpit/accounting_post_row.rb +++ b/app/domain/order/cockpit/accounting_post_row.rb @@ -1,95 +1,97 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Order::Cockpit - class AccountingPostRow < Row - attr_reader :cells, :accounting_post - - def initialize(accounting_post, label = nil) - super(label || accounting_post.to_s) - @accounting_post = accounting_post - @cells = build_cells - end - - def portfolio - accounting_post.portfolio_item.to_s - end - - def offered_rate - accounting_post.offered_rate - end - - def supplied_services_hours - accounting_post_hours.values.sum - end - - def billable_hours - accounting_post_hours[true] || 0 - end - - def not_billable_hours - accounting_post_hours[false] || 0 - end - - def shortnames - accounting_post.work_item.path_shortnames - end - - def name - accounting_post.work_item.name - end - - def future_plannings - accounting_post.work_item.plannings.definitive.where('date > ?', Date.today) - end - - def future_planned_hours - future_plannings.sum(0) { |planning| planning.hours } - end - - private - - def build_cells - { budget: build_budget_cell, - supplied_services: build_supplied_services_cell, - not_billable: build_not_billable_cell, - open_budget: build_open_budget_cell, - planned_budget: build_planned_budget_cell } - end - - def build_budget_cell - Cell.new(accounting_post.offered_hours, accounting_post.offered_total) - end - - def build_supplied_services_cell - build_cell_with_amount(supplied_services_hours) - end - - def build_not_billable_cell - Cell.new(not_billable_hours, calculate_amount(not_billable_hours)) - end - - def build_open_budget_cell - hours = (accounting_post.offered_hours || 0) - billable_hours - build_cell_with_amount(hours) - end - - def build_planned_budget_cell - build_cell_with_amount(future_planned_hours) - end - - def build_cell_with_amount(hours) - Cell.new(hours, calculate_amount(hours)) - end - - def calculate_amount(hours) - offered_rate && offered_rate * hours.to_d - end - - def accounting_post_hours - @hours ||= accounting_post.worktimes.group(:billable).sum(:hours) +class Order + class Cockpit + class AccountingPostRow < Row + attr_reader :cells, :accounting_post + + def initialize(accounting_post, label = nil) + super(label || accounting_post.to_s) + @accounting_post = accounting_post + @cells = build_cells + end + + def portfolio + accounting_post.portfolio_item.to_s + end + + delegate :offered_rate, to: :accounting_post + + def supplied_services_hours + accounting_post_hours.values.sum + end + + def billable_hours + accounting_post_hours[true] || 0 + end + + def not_billable_hours + accounting_post_hours[false] || 0 + end + + def shortnames + accounting_post.work_item.path_shortnames + end + + def name + accounting_post.work_item.name + end + + def future_plannings + accounting_post.work_item.plannings.definitive.where('date > ?', Time.zone.today) + end + + def future_planned_hours + future_plannings.sum(&:hours) + end + + private + + def build_cells + { budget: build_budget_cell, + supplied_services: build_supplied_services_cell, + not_billable: build_not_billable_cell, + open_budget: build_open_budget_cell, + planned_budget: build_planned_budget_cell } + end + + def build_budget_cell + Cell.new(accounting_post.offered_hours, accounting_post.offered_total) + end + + def build_supplied_services_cell + build_cell_with_amount(supplied_services_hours) + end + + def build_not_billable_cell + Cell.new(not_billable_hours, calculate_amount(not_billable_hours)) + end + + def build_open_budget_cell + hours = (accounting_post.offered_hours || 0) - billable_hours + build_cell_with_amount(hours) + end + + def build_planned_budget_cell + build_cell_with_amount(future_planned_hours) + end + + def build_cell_with_amount(hours) + Cell.new(hours, calculate_amount(hours)) + end + + def calculate_amount(hours) + offered_rate && (offered_rate * hours.to_d) + end + + def accounting_post_hours + @hours ||= accounting_post.worktimes.group(:billable).sum(:hours) + end end end end diff --git a/app/domain/order/cockpit/cell.rb b/app/domain/order/cockpit/cell.rb index ab142ce5e..8080db5c0 100644 --- a/app/domain/order/cockpit/cell.rb +++ b/app/domain/order/cockpit/cell.rb @@ -1,16 +1,20 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Order::Cockpit - class Cell < Struct.new(:hours, :amount) - def days - hours / must_hours_per_day if hours - end +class Order + class Cockpit + Cell = Struct.new(:hours, :amount) do + def days + hours / must_hours_per_day if hours + end - def must_hours_per_day - WorkingCondition.todays_value(:must_hours_per_day) + def must_hours_per_day + WorkingCondition.todays_value(:must_hours_per_day) + end end end end diff --git a/app/domain/order/cockpit/row.rb b/app/domain/order/cockpit/row.rb index df05ed9bd..6eae1443d 100644 --- a/app/domain/order/cockpit/row.rb +++ b/app/domain/order/cockpit/row.rb @@ -1,10 +1,14 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Order::Cockpit - class Row < Struct.new(:label) - attr_reader :cells +class Order + class Cockpit + Row = Struct.new(:label) do + attr_reader :cells + end end end diff --git a/app/domain/order/cockpit/total_row.rb b/app/domain/order/cockpit/total_row.rb index 2917dcad8..1964eddbb 100644 --- a/app/domain/order/cockpit/total_row.rb +++ b/app/domain/order/cockpit/total_row.rb @@ -1,34 +1,36 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Order::Cockpit - class TotalRow < Row - def initialize(rows) - super('Total') - @cells = build_total_cells(rows) - end +class Order + class Cockpit + class TotalRow < Row + def initialize(rows) + super('Total') + @cells = build_total_cells(rows) + end - private + private - def build_total_cells(rows) - cells = rows.collect(&:cells) - master = cells.first - hash = {} - if master - master.keys.each do |key| + def build_total_cells(rows) + cells = rows.collect(&:cells) + master = cells.first + hash = {} + master&.each_key do |key| hash[key] = Cell.new(sum_non_nil_values(cells, key, :hours, :to_d), sum_non_nil_values(cells, key, :amount, :to_d)) end + hash end - hash - end - def sum_non_nil_values(cells, key, field, converter) - values = cells.collect { |c| c[key].send(field) } - unless values.all?(&:nil?) + def sum_non_nil_values(cells, key, field, converter) + values = cells.collect { |c| c[key].send(field) } + return if values.all?(&:nil?) + values.sum(&converter) end end diff --git a/app/domain/order/controlling.rb b/app/domain/order/controlling.rb index 4c761e2cf..26fb019f6 100644 --- a/app/domain/order/controlling.rb +++ b/app/domain/order/controlling.rb @@ -1,118 +1,118 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Order::Controlling - attr_reader :order, :date +class Order + class Controlling + attr_reader :order, :date - def initialize(order, date = Time.zone.now) - @order = order - @date = date - end + def initialize(order, date = Time.zone.now) + @order = order + @date = date + end - def offered_total - order.accounting_posts.sum(:offered_total) - end + def offered_total + order.accounting_posts.sum(:offered_total) + end - def efforts_per_week - {}.tap do |result| - grouped_worktimes.each { |e| add_worktime(result, e) } - grouped_plannings.each { |e| add_planning(result, e) } - fill_week_gaps!(result) + def efforts_per_week + {}.tap do |result| + grouped_worktimes.each { |e| add_worktime(result, e) } + grouped_plannings.each { |e| add_planning(result, e) } + fill_week_gaps!(result) + end end - end - def efforts_per_week_cumulated - efforts = efforts_per_week - efforts.keys.sort.each_cons(2) do |previous_week, week| - efforts[week] = sum_entries(efforts[previous_week], efforts[week]) + def efforts_per_week_cumulated + efforts = efforts_per_week + efforts.keys.sort.each_cons(2) do |previous_week, week| + efforts[week] = sum_entries(efforts[previous_week], efforts[week]) + end + efforts end - efforts - end - private + private - def grouped_worktimes - load_worktimes - .group('week, worktimes.billable') - .order('week') - .pluck('DATE_TRUNC(\'week\', work_date) week, worktimes.billable, SUM(hours * offered_rate)') - end + def grouped_worktimes + load_worktimes + .group('week, worktimes.billable') + .order('week') + .pluck(Arel.sql('DATE_TRUNC(\'week\', work_date) week, worktimes.billable, SUM(hours * offered_rate)')) + end - def grouped_plannings - load_plannings - .in_period(Period.with(date, nil)) - .group('week, offered_rate, definitive') - .order('week') - .pluck('DATE_TRUNC(\'week\', date) week, offered_rate, definitive, SUM(percent)') - end + def grouped_plannings + load_plannings + .in_period(Period.with(date, nil)) + .group('week, offered_rate, definitive') + .order('week') + .pluck(Arel.sql('DATE_TRUNC(\'week\', date) week, offered_rate, definitive, SUM(percent)')) + end - def load_worktimes - order - .worktimes - .joins('INNER JOIN accounting_posts ON accounting_posts.work_item_id = work_items.id') - end + def load_worktimes + order + .worktimes + .joins('INNER JOIN accounting_posts ON accounting_posts.work_item_id = work_items.id') + end - def load_plannings - Planning - .joins(work_item: :accounting_post) - .joins('LEFT JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)') - .where('orders.id = ?', order.id) - end + def load_plannings + Planning + .joins(work_item: :accounting_post) + .joins('LEFT JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)') + .where(orders: { id: order.id }) + end - def add_worktime(result, entry) - week, billable, hours = entry - hours = 0.0 if hours.nil? - add_value(result, week, billable ? :billable : :unbillable, hours) - end + def add_worktime(result, entry) + week, billable, hours = entry + hours = 0.0 if hours.nil? + add_value(result, week, billable ? :billable : :unbillable, hours) + end - def add_planning(result, entry) - week, offered_rate, definitive, percent = entry - must_hours = WorkingCondition.value_at(week, :must_hours_per_day).to_f - effort = percent / 100.0 * must_hours * offered_rate.to_f - add_value(result, week, definitive ? :planned_definitive : :planned_provisional, effort) - end + def add_planning(result, entry) + week, offered_rate, definitive, percent = entry + must_hours = WorkingCondition.value_at(week, :must_hours_per_day).to_f + effort = percent / 100.0 * must_hours * offered_rate.to_f + add_value(result, week, definitive ? :planned_definitive : :planned_provisional, effort) + end - def add_value(result, week, key, value) - unless result[week] - result[week] = empty_entry + def add_value(result, week, key, value) + result[week] = empty_entry unless result[week] + new_entry = empty_entry.tap { |e| e[key] = value || 0.0 } + result[week] = sum_entries(result[week], new_entry) end - new_entry = empty_entry.tap { |e| e[key] = value ? value : 0.0 } - result[week] = sum_entries(result[week], new_entry) - end - def sum_entries(a, b) - result = empty_entry - entry_keys.each do |key| - result[key] = a[key] + b[key] + def sum_entries(a, b) + result = empty_entry + entry_keys.each do |key| + result[key] = a[key] + b[key] + end + result end - result - end - def empty_entry - {}.tap { |e| entry_keys.each { |k| e[k] = 0.0 } } - end + def empty_entry + {}.tap { |e| entry_keys.each { |k| e[k] = 0.0 } } + end - def entry_keys - [:billable, :unbillable, :planned_definitive, :planned_provisional] - end + def entry_keys + %i[billable unbillable planned_definitive planned_provisional] + end - def fill_week_gaps!(efforts) - dates = efforts.keys.sort - return if dates.size < 2 + def fill_week_gaps!(efforts) + dates = efforts.keys.sort + return if dates.size < 2 - for_each_week(dates.first, dates.last) do |week| - unless dates.include?(week) - efforts[week] = empty_entry + for_each_week(dates.first, dates.last) do |week| + efforts[week] = empty_entry unless dates.include?(week) end + efforts end - efforts - end - def for_each_week(from, to) - (from.beginning_of_week.to_date..to.beginning_of_week.to_date) - .group_by { |d| "#{d.year}#{d.cweek}" } - .each { |_, a| yield a.first.to_time(:utc) } + def for_each_week(from, to) + (from.beginning_of_week.to_date..to.beginning_of_week.to_date) + .group_by { |d| "#{d.year}#{d.cweek}" } + .each_value { |a| yield a.first.to_time(:utc) } + end end end diff --git a/app/domain/order/copier.rb b/app/domain/order/copier.rb index f357c910e..566d95398 100644 --- a/app/domain/order/copier.rb +++ b/app/domain/order/copier.rb @@ -1,58 +1,62 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Order::Copier - attr_reader :source, :copy +class Order + class Copier + attr_reader :source, :copy - def initialize(source) - @source = source - end + def initialize(source) + @source = source + end - # Copies an order together with everything that is edited in the order form. - def copy - @copy = source.dup - @copy.work_item = source.work_item.dup - @copy.work_item.parent_id = source.work_item.parent_id - @copy.order_contacts = source.order_contacts.collect(&:dup) - @copy.order_team_members = source.order_team_members.collect(&:dup) - @copy.crm_key = nil - @copy.status_id = nil - @copy.set_default_status_id - @copy - end + # Copies an order together with everything that is edited in the order form. + def copy + @copy = source.dup + @copy.work_item = source.work_item.dup + @copy.work_item.parent_id = source.work_item.parent_id + @copy.order_contacts = source.order_contacts.collect(&:dup) + @copy.order_team_members = source.order_team_members.collect(&:dup) + @copy.crm_key = nil + @copy.status_id = nil + @copy.set_default_status_id + @copy + end - # Copies all order associations not edited in the order form. - def copy_associations(target) - target.work_item.order = target - target.contract = source.contract.try(:dup) + # Copies all order associations not edited in the order form. + def copy_associations(target) + target.work_item.order = target + target.contract = source.contract.try(:dup) - copy_work_item_children(source.work_item, target.work_item) - end + copy_work_item_children(source.work_item, target.work_item) + end - private + private - def copy_work_item_children(source, target) - copy_accounting_post(source.accounting_post, target) + def copy_work_item_children(source, target) + copy_accounting_post(source.accounting_post, target) - source.children.each do |child| - copy = child.dup - copy.parent = target - target.children << copy + source.children.each do |child| + copy = child.dup + copy.parent = target + target.children << copy - copy_work_item_children(child, copy) + copy_work_item_children(child, copy) + end end - end - def copy_accounting_post(source, work_item) - return if source.nil? + def copy_accounting_post(source, work_item) + return if source.nil? - copy = source.dup - copy.work_item = work_item - copy.closed = false - work_item.accounting_post = copy + copy = source.dup + copy.work_item = work_item + copy.closed = false + work_item.accounting_post = copy - copy + copy + end end end diff --git a/app/domain/order/report.rb b/app/domain/order/report.rb index 03994d6ca..d9e60f0af 100644 --- a/app/domain/order/report.rb +++ b/app/domain/order/report.rb @@ -1,231 +1,229 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Order::Report - include Filterable +class Order + class Report + include Filterable - attr_reader :period, :params + attr_reader :period, :params - def initialize(period, params = {}) - @period = period - @params = params - end + def initialize(period, params = {}) + @period = period + @params = params + end - def page(&block) - section = entries[((current_page - 1) * limit_value)...(current_page * limit_value)] - ([total] + section).each(&block) if section.present? - end + def page(&) + section = entries[((current_page - 1) * limit_value)...(current_page * limit_value)] + ([total] + section).each(&) if section.present? + end - def entries - @entries ||= sort_entries(load_entries) - end + def entries + @entries ||= sort_entries(load_entries) + end - def total - @total ||= Order::Report::Total.new(self) - end + def total + @total ||= Order::Report::Total.new(self) + end - def current_page - (params[:page] || 1).to_i - end + def current_page + (params[:page] || 1).to_i + end - def total_pages - (entries.size / limit_value.to_f).ceil - end + def total_pages + (entries.size / limit_value.to_f).ceil + end - def limit_value - 20 - end + def limit_value + 20 + end - def present? - entries.present? - end + delegate :present?, to: :entries - def filters_defined? - filters = params.except(:action, :controller, :format, :utf8, :page, - :clear, :closed) - filters.present? && filters.values.any?(&:present?) - end + def filters_defined? + filters = params.except(:action, :controller, :format, :utf8, :page, + :clear, :closed) + filters.present? && filters.values.any?(&:present?) + end - private + private - def load_entries - orders = load_orders.to_a - accounting_posts = accounting_posts_to_hash(load_accounting_posts(orders)) - hours = hours_to_hash(load_accounting_post_hours(accounting_posts.values)) - invoices = invoices_to_hash(load_invoices(orders)) - orders.collect { |o| build_entry(o, accounting_posts, hours, invoices) }.compact - end + def load_entries + orders = load_orders.to_a + accounting_posts = accounting_posts_to_hash(load_accounting_posts(orders)) + hours = hours_to_hash(load_accounting_post_hours(accounting_posts.values)) + invoices = invoices_to_hash(load_invoices(orders)) + orders.filter_map { |o| build_entry(o, accounting_posts, hours, invoices) } + end - def load_orders - entries = Order.list.includes(:status, :targets, :order_uncertainties) - entries = filter_by_closed(entries) - entries = filter_by_parent(entries) - entries = filter_by_target(entries) - entries = filter_by_uncertainty(entries, :major_risk_value) - entries = filter_by_uncertainty(entries, :major_chance_value) - filter_entries_by(entries, :kind_id, :responsible_id, :status_id, :department_id) - end + def load_orders + entries = Order.list.includes(:status, :targets, :order_uncertainties) + entries = filter_by_closed(entries) + entries = filter_by_parent(entries) + entries = filter_by_target(entries) + entries = filter_by_uncertainty(entries, :major_risk_value) + entries = filter_by_uncertainty(entries, :major_chance_value) + filter_entries_by(entries, :kind_id, :responsible_id, :status_id, :department_id) + end - def load_accounting_posts(orders) - AccountingPost.joins(:work_item). - joins('INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)'). - where(orders: { id: orders.collect(&:id) }). - pluck('orders.id, accounting_posts.id, accounting_posts.offered_total, ' \ - 'accounting_posts.offered_rate, accounting_posts.offered_hours') - end + def load_accounting_posts(orders) + AccountingPost.joins(:work_item) + .joins('INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)') + .where(orders: { id: orders.collect(&:id) }) + .pluck('orders.id, accounting_posts.id, accounting_posts.offered_total, ' \ + 'accounting_posts.offered_rate, accounting_posts.offered_hours') + end - def accounting_posts_to_hash(result) - result.each_with_object(Hash.new { |h, k| h[k] = {} }) do |row, hash| - hash[row.first][row[1]] = { offered_total: row[2], - offered_rate: row[3], - offered_hours: row[4] } + def accounting_posts_to_hash(result) + result.each_with_object(Hash.new { |h, k| h[k] = {} }) do |row, hash| + hash[row.first][row[1]] = { offered_total: row[2], + offered_rate: row[3], + offered_hours: row[4] } + end end - end - def load_accounting_post_hours(accounting_posts) - accounting_post_hours = - Worktime - .joins(:work_item) - .joins('INNER JOIN accounting_posts ON ' \ - 'accounting_posts.work_item_id = ANY (work_items.path_ids)') - .where(accounting_posts: { id: accounting_posts.collect(&:keys).flatten }) + def load_accounting_post_hours(accounting_posts) + accounting_post_hours = + Worktime + .joins(:work_item) + .joins('INNER JOIN accounting_posts ON ' \ + 'accounting_posts.work_item_id = ANY (work_items.path_ids)') + .where(accounting_posts: { id: accounting_posts.collect(&:keys).flatten }) - if params[:closed].blank? - accounting_post_hours = accounting_post_hours.in_period(period) - end + accounting_post_hours = accounting_post_hours.in_period(period) if params[:closed].blank? - accounting_post_hours. - group('accounting_posts.id, worktimes.billable'). - pluck('accounting_posts.id, worktimes.billable, SUM(worktimes.hours)') - end + accounting_post_hours + .group('accounting_posts.id, worktimes.billable') + .pluck('accounting_posts.id, worktimes.billable, SUM(worktimes.hours)') + end - def hours_to_hash(result) - result.each_with_object(Hash.new { |h, k| h[k] = Hash.new(0) }) do |row, hash| - hash[row.first][row.second] = row.last + def hours_to_hash(result) + result.each_with_object(Hash.new { |h, k| h[k] = Hash.new(0) }) do |row, hash| + hash[row.first][row.second] = row.last + end end - end - def load_invoices(orders) - invoices = Invoice.where(order_id: orders.collect(&:id)) + def load_invoices(orders) + invoices = Invoice.where(order_id: orders.collect(&:id)) - if params[:closed].blank? - invoices = invoices.where(period.where_condition('billing_date')) - end + invoices = invoices.where(period.where_condition('billing_date')) if params[:closed].blank? - invoices - .group('order_id') - .pluck('order_id, SUM(total_amount) AS total_amount, SUM(total_hours) AS total_hours') - end + invoices + .group('order_id') + .pluck('order_id, SUM(total_amount) AS total_amount, SUM(total_hours) AS total_hours') + end - def invoices_to_hash(result) - result.each_with_object(Hash.new { |h, k| h[k] = Hash.new(0) }) do |row, hash| - hash[row.first][:total_amount] = row[1] - hash[row.first][:total_hours] = row[2] + def invoices_to_hash(result) + result.each_with_object(Hash.new { |h, k| h[k] = Hash.new(0) }) do |row, hash| + hash[row.first][:total_amount] = row[1] + hash[row.first][:total_hours] = row[2] + end end - end - def build_entry(order, accounting_posts, hours, invoices) - posts = accounting_posts[order.id] - post_hours = hours.slice(*posts.keys) - if post_hours.values.any? { |h| h.values.sum > 0.0001 } + def build_entry(order, accounting_posts, hours, invoices) + posts = accounting_posts[order.id] + post_hours = hours.slice(*posts.keys) + return unless post_hours.values.any? { |h| h.values.sum > 0.0001 } + Order::Report::Entry.new(order, posts, post_hours, invoices[order.id]) end - end - def filter_by_parent(orders) - if params[:category_work_item_id].present? - orders.where('? = ANY (work_items.path_ids)', params[:category_work_item_id]) - elsif params[:client_work_item_id].present? - orders.where('? = ANY (work_items.path_ids)', params[:client_work_item_id]) - else - orders + def filter_by_parent(orders) + if params[:category_work_item_id].present? + orders.where('? = ANY (work_items.path_ids)', params[:category_work_item_id]) + elsif params[:client_work_item_id].present? + orders.where('? = ANY (work_items.path_ids)', params[:client_work_item_id]) + else + orders + end end - end - def filter_by_target(orders) - if params[:target].present? - ratings = params[:target].split('_') - orders.joins('LEFT JOIN order_targets filter_targets ON filter_targets.order_id = orders.id'). - where(filter_targets: { rating: ratings }) - else - orders + def filter_by_target(orders) + if params[:target].present? + ratings = params[:target].split('_') + orders.joins('LEFT JOIN order_targets filter_targets ON filter_targets.order_id = orders.id') + .where(filter_targets: { rating: ratings }) + else + orders + end end - end - - def filter_by_closed(orders) - return orders if params[:closed].blank? - orders - .where(order_statuses: { closed: true }) - .where(period.where_condition('closed_at')) - end + def filter_by_closed(orders) + return orders if params[:closed].blank? - def filter_by_uncertainty(orders, attr) - if params[attr].present? - orders.where(attr => map_uncertainties_filter(params[attr])) - else orders + .where(order_statuses: { closed: true }) + .where(period.where_condition('closed_at')) end - end - def map_uncertainties_filter(value) - case value - when 'low' - 0..2 - when 'medium' - 3..7 - when 'high' - 8..16 + def filter_by_uncertainty(orders, attr) + if params[attr].present? + orders.where(attr => map_uncertainties_filter(params[attr])) + else + orders + end end - end - def sort_entries(entries) - dir = params[:sort_dir].to_s.casecmp('desc').zero? ? 1 : -1 - match = sort_by_target? - if match - sort_by_target(entries, match[1], dir) - elsif sort_by_string? - sort_by_string(entries, dir) - elsif sort_by_number? - sort_by_number(entries, dir) - else - entries + def map_uncertainties_filter(value) + case value + when 'low' + 0..2 + when 'medium' + 3..7 + when 'high' + 8..16 + end end - end - def sort_by_string? - %w(client).include?(params[:sort]) - end + def sort_entries(entries) + dir = params[:sort_dir].to_s.casecmp('desc').zero? ? 1 : -1 + match = sort_by_target? + if match + sort_by_target(entries, match[1], dir) + elsif sort_by_string? + sort_by_string(entries, dir) + elsif sort_by_number? + sort_by_number(entries, dir) + else + entries + end + end - def sort_by_number? - Order::Report::Entry.public_instance_methods(false) - .collect(&:to_s) - .include?(params[:sort]) - end + def sort_by_string? + %w[client].include?(params[:sort]) + end - def sort_by_target? - params[:sort].to_s.match(/\Atarget_scope_(\d+)\z/) - end + def sort_by_number? + Order::Report::Entry.public_instance_methods(false) + .collect(&:to_s) + .include?(params[:sort]) + end - def sort_by_string(entries, dir) - sorted = entries.sort_by do |e| - e.send(params[:sort]) + def sort_by_target? + params[:sort].to_s.match(/\Atarget_scope_(\d+)\z/) end - sorted.reverse! if dir > 0 - sorted - end - def sort_by_number(entries, dir) - entries.sort_by do |e| - e.send(params[:sort]).to_f * dir + def sort_by_string(entries, dir) + sorted = entries.sort_by do |e| + e.send(params[:sort]) + end + sorted.reverse! if dir.positive? + sorted + end + + def sort_by_number(entries, dir) + entries.sort_by do |e| + e.send(params[:sort]).to_f * dir + end end - end - def sort_by_target(entries, target_scope_id, dir) - entries.sort_by do |e| - dir * OrderTarget::RATINGS.index(e.target(target_scope_id).try(:rating)).to_i + def sort_by_target(entries, target_scope_id, dir) + entries.sort_by do |e| + dir * OrderTarget::RATINGS.index(e.target(target_scope_id).try(:rating)).to_i + end end end end diff --git a/app/domain/order/report/bi.rb b/app/domain/order/report/bi.rb deleted file mode 100644 index 53f1ab9ab..000000000 --- a/app/domain/order/report/bi.rb +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -class Order::Report::BI - attr_reader :report - - TAGS = %i[client category name status] - TARGET_TAGS = { - 'Kosten' => :target_budget, - 'Termin' => :target_schedule, - 'Qualität' => :target_quality - }.freeze - - METRICS = %i[ - offered_amount - supplied_amount - billable_amount - billed_amount - billability - offered_rate - billed_rate - average_rate - ] - - def initialize(departments = all_departments) - @departments = departments - end - - def stats - @departments.flat_map { |d| department_stats(d) } - end - - private - - def all_departments - Department.having_employees - end - - def department_stats(department) - period = Period.new(nil, nil) - status_ids = OrderStatus.open.pluck(:id) - report = - Order::Report.new( - period, - department_id: department.id, status_id: status_ids - ) - - targets = target_scopes - - report.entries - report.entries.flat_map { |e| entry_stats(e, department, targets) } - end - - def entry_stats(entry, department, targets) - fields = - METRICS.each_with_object({}) do |metric, memo| - memo[metric] = entry.send(metric) - end - - { - name: 'order_report', - fields: fields, - tags: tags(entry, department, targets) - } - end - - def tags(entry, department, targets) - { department: department.to_s }.merge(basic_tags(entry)).merge( - rating_tags(entry, targets) - ) - end - - def basic_tags(entry) - TAGS.each_with_object({}) { |tag, memo| memo[tag] = entry.send(tag).to_s } - end - - def rating_tags(entry, targets) - targets.each_with_object({}) do |target, memo| - rating = entry.target(target.id).try(:rating) || 'none' - tag = TARGET_TAGS.fetch(target.name) - memo[tag] = rating - end - end - - def target_scopes - TargetScope.list.to_a - end -end diff --git a/app/domain/order/report/csv.rb b/app/domain/order/report/csv.rb index e6fa337f4..34f0cc5ae 100644 --- a/app/domain/order/report/csv.rb +++ b/app/domain/order/report/csv.rb @@ -1,44 +1,50 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Order::Report::Csv - attr_reader :report +class Order + class Report + class Csv + attr_reader :report - def initialize(report) - @report = report - end + def initialize(report) + @report = report + end - def generate - report.entries - CSV.generate do |csv| - csv << header + def generate + report.entries + CSV.generate do |csv| + csv << header - report.entries.each do |e| - csv << row(e) + report.entries.each do |e| + csv << row(e) + end + end end - end - end - private + private - def header - ['Kunde', 'Kategorie', 'Auftrag', 'Status', 'Abgeschlossen am', 'Budget', - 'Geleistet', 'Verrechenbar', 'Verrechnet', 'Verrechenbarkeit', - 'Offerierter Stundensatz', 'Verrechnete Stundensatz', - 'Durchschnittlicher Stundensatz', *target_scopes.collect(&:name)] - end + def header + ['Kunde', 'Kategorie', 'Auftrag', 'Status', 'Abgeschlossen am', 'Budget', + 'Geleistet', 'Verrechenbar', 'Verrechnet', 'Verrechenbarkeit', + 'Offerierter Stundensatz', 'Verrechnete Stundensatz', + 'Durchschnittlicher Stundensatz', *target_scopes.collect(&:name)] + end - def row(e) - ratings = target_scopes.collect { |scope| e.target(scope.id).try(:rating) } + def row(e) + ratings = target_scopes.collect { |scope| e.target(scope.id).try(:rating) } - [e.client, e.category, e.name, e.status.to_s, e.closed_at, - e.offered_amount, e.supplied_amount, e.billable_amount, e.billed_amount, - e.billability, e.offered_rate, e.billed_rate, e.average_rate, *ratings] - end + [e.client, e.category, e.name, e.status.to_s, e.closed_at, + e.offered_amount, e.supplied_amount, e.billable_amount, e.billed_amount, + e.billability, e.offered_rate, e.billed_rate, e.average_rate, *ratings] + end - def target_scopes - @target_scopes ||= TargetScope.list.to_a + def target_scopes + @target_scopes ||= TargetScope.list.to_a + end + end end end diff --git a/app/domain/order/report/entry.rb b/app/domain/order/report/entry.rb index cf1559042..12d5c5e26 100644 --- a/app/domain/order/report/entry.rb +++ b/app/domain/order/report/entry.rb @@ -1,110 +1,116 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Order::Report::Entry < SimpleDelegator - attr_reader :order, :accounting_posts, :hours, :invoices +class Order + class Report + class Entry < SimpleDelegator + attr_reader :order, :accounting_posts, :hours, :invoices - delegate :major_chance, :major_chance_value, :major_risk, :major_risk_value, to: :order + delegate :major_chance, :major_chance_value, :major_risk, :major_risk_value, to: :order - def initialize(order, accounting_posts, hours, invoices) - super(order) - @order = order - @accounting_posts = accounting_posts - @hours = hours - @invoices = invoices - end + def initialize(order, accounting_posts, hours, invoices) + super(order) + @order = order + @accounting_posts = accounting_posts + @hours = hours + @invoices = invoices + end - def client - work_item.path_names.lines.to_a.first.strip - end + def client + work_item.path_names.lines.to_a.first.strip + end - def category - work_item.path_ids.size > 2 ? work_item.path_names.lines.to_a.second.strip : nil - end + def category + work_item.path_ids.size > 2 ? work_item.path_names.lines.to_a.second.strip : nil + end - def offered_amount - @offered ||= sum_accounting_posts { |id| post_value(id, :offered_total) } - end + def offered_amount + @offered ||= sum_accounting_posts { |id| post_value(id, :offered_total) } + end - def offered_rate - @offered_rate ||= - if offered_hours > 0 - (offered_amount / offered_hours).to_d - else - rates = sum_accounting_posts { |id| post_value(id, :offered_rate) } - rates > 0 ? rates / accounting_posts.size : nil + def offered_rate + @offered_rate ||= + if offered_hours.positive? + (offered_amount / offered_hours).to_d + else + rates = sum_accounting_posts { |id| post_value(id, :offered_rate) } + rates.positive? ? rates / accounting_posts.size : nil + end end - end - def offered_hours - @offered_hours ||= sum_accounting_posts { |id| post_value(id, :offered_hours) } - end + def offered_hours + @offered_hours ||= sum_accounting_posts { |id| post_value(id, :offered_hours) } + end - def supplied_amount - @supplied ||= sum_accounting_posts { |id| post_value(id, :offered_rate) * post_hours(id) } - end + def supplied_amount + @supplied ||= sum_accounting_posts { |id| post_value(id, :offered_rate) * post_hours(id) } + end - def supplied_hours - @supplied_hours ||= sum_accounting_posts { |id| post_hours(id) } - end + def supplied_hours + @supplied_hours ||= sum_accounting_posts { |id| post_hours(id) } + end - def billable_amount - @billable ||= sum_accounting_posts { |id| post_value(id, :offered_rate) * post_hours(id, true) } - end + def billable_amount + @billable ||= sum_accounting_posts { |id| post_value(id, :offered_rate) * post_hours(id, true) } + end - def billable_hours - @billable_hours ||= sum_accounting_posts { |id| post_hours(id, true) } - end + def billable_hours + @billable_hours ||= sum_accounting_posts { |id| post_hours(id, true) } + end - def billed_amount - invoices[:total_amount].to_d - end + def billed_amount + invoices[:total_amount].to_d + end - def billed_hours - invoices[:total_hours].to_d - end + def billed_hours + invoices[:total_hours].to_d + end - def billability - @billability ||= supplied_hours > 0 ? (billable_hours / supplied_hours * 100).round : nil - end + def billability + @billability ||= supplied_hours.positive? ? (billable_hours / supplied_hours * 100).round : nil + end - def billed_rate - @billed_rate ||= billable_hours > 0 ? billed_amount / billable_hours : nil - end + def billed_rate + @billed_rate ||= billable_hours.positive? ? billed_amount / billable_hours : nil + end - def average_rate - @average_rate ||= supplied_hours > 0 ? billable_amount / supplied_hours : nil - end + def average_rate + @average_rate ||= supplied_hours.positive? ? billable_amount / supplied_hours : nil + end - def target(scope_id) - targets.find { |t| t.target_scope_id == scope_id.to_i } - end + def target(scope_id) + targets.find { |t| t.target_scope_id == scope_id.to_i } + end - private + private - def sum_accounting_posts(&block) - accounting_posts.keys.sum(&block) - end + def sum_accounting_posts(&) + accounting_posts.keys.sum(&) + end - def post_hours(id, billable = nil) - h = hours[id] - return 0.to_d unless h + def post_hours(id, billable = nil) + h = hours[id] + return BigDecimal('0') unless h - if billable.nil? - h.values.sum.to_d - else - h[billable].to_d - end - end + if billable.nil? + h.values.sum.to_d + else + h[billable].to_d + end + end - def post_value(id, key) - accounting_posts[id][key] || 0 - end + def post_value(id, key) + accounting_posts[id][key] || 0 + end - # caching these explicitly gives quite a performance benefit if many orders are exported - def targets - @targets ||= order.targets.to_a + # caching these explicitly gives quite a performance benefit if many orders are exported + def targets + @targets ||= order.targets.to_a + end + end end end diff --git a/app/domain/order/report/total.rb b/app/domain/order/report/total.rb index 86c9f3cec..b82a7c2aa 100644 --- a/app/domain/order/report/total.rb +++ b/app/domain/order/report/total.rb @@ -1,98 +1,102 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Order::Report::Total - delegate :entries, to: :@report +class Order + class Report + class Total + delegate :entries, to: :@report - def initialize(report) - @report = report - end + def initialize(report) + @report = report + end - def parent_names - end + def parent_names; end - def to_s - "Total (#{entries.count})" - end + def to_s + "Total (#{entries.count})" + end - def order - nil - end + def order + nil + end - def status - nil - end + def status + nil + end - def closed_at - nil - end + def closed_at + nil + end - def targets - [] - end + def targets + [] + end - def major_chance - nil - end + def major_chance + nil + end - def major_risk - nil - end + def major_risk + nil + end - def offered_amount - @offered_amount ||= entries.sum(&:offered_amount) - end + def offered_amount + @offered_amount ||= entries.sum(&:offered_amount) + end - def offered_hours - @offered_hours ||= entries.sum(&:offered_hours) - end + def offered_hours + @offered_hours ||= entries.sum(&:offered_hours) + end - def supplied_amount - entries.sum(&:supplied_amount) - end + def supplied_amount + entries.sum(&:supplied_amount) + end - def supplied_hours - @supplied_hours ||= entries.sum(&:supplied_hours) - end + def supplied_hours + @supplied_hours ||= entries.sum(&:supplied_hours) + end - def billable_amount - entries.sum(&:billable_amount) - end + def billable_amount + entries.sum(&:billable_amount) + end - def billable_hours - entries.sum(&:billable_hours) - end + def billable_hours + entries.sum(&:billable_hours) + end - def billed_amount - @billed_amount ||= entries.sum(&:billed_amount) - end + def billed_amount + @billed_amount ||= entries.sum(&:billed_amount) + end - def billed_hours - @billed_hours ||= entries.sum(&:billed_hours) - end + def billed_hours + @billed_hours ||= entries.sum(&:billed_hours) + end - def offered_rate - if offered_hours > 0 - (offered_amount / offered_hours).to_d - elsif !entries.empty? - entries.collect(&:offered_rate).compact.sum / entries.size - end - end + def offered_rate + if offered_hours.positive? + (offered_amount / offered_hours).to_d + elsif !entries.empty? + entries.filter_map(&:offered_rate).sum / entries.size + end + end - def billability - @billability ||= supplied_hours > 0 ? (billable_hours / supplied_hours * 100).round : nil - end + def billability + @billability ||= supplied_hours.positive? ? (billable_hours / supplied_hours * 100).round : nil + end - def billed_rate - @billed_rate ||= billable_hours > 0 ? billed_amount / billable_hours : nil - end + def billed_rate + @billed_rate ||= billable_hours.positive? ? billed_amount / billable_hours : nil + end - def average_rate - @average_rate ||= supplied_hours > 0 ? billable_amount / supplied_hours : nil - end + def average_rate + @average_rate ||= supplied_hours.positive? ? billable_amount / supplied_hours : nil + end - def target(_id) + def target(_id); end + end end end diff --git a/app/domain/order/services/csv_filename_generator.rb b/app/domain/order/services/csv_filename_generator.rb index 113ab94e9..920440da6 100644 --- a/app/domain/order/services/csv_filename_generator.rb +++ b/app/domain/order/services/csv_filename_generator.rb @@ -1,52 +1,56 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -module Order::Services - class CsvFilenameGenerator - attr_reader :order, :params +class Order + module Services + class CsvFilenameGenerator + attr_reader :order, :params - def initialize(order, params = {}) - @order = order - @params = params - end + def initialize(order, params = {}) + @order = order + @params = params + end - def filename - ['puzzletime', - accounting_post_shortnames || order_shortnames, - employee_shortname, - ticket, - billable] - .compact - .join('-') + - '.csv' - end + def filename + ['puzzletime', + accounting_post_shortnames || order_shortnames, + employee_shortname, + ticket, + billable] + .compact + .join('-') + + '.csv' + end - private + private - def order_shortnames - order.work_item.path_shortnames - end + def order_shortnames + order.work_item.path_shortnames + end + + def accounting_post_shortnames + return if params[:work_item_id].blank? - def accounting_post_shortnames - if params[:work_item_id].present? WorkItem.find(params[:work_item_id]).path_shortnames end - end - def employee_shortname - if params[:employee_id].present? + def employee_shortname + return if params[:employee_id].blank? + Employee.find(params[:employee_id]).shortname end - end - def billable - "billable_#{params[:billable]}" if params[:billable].present? - end + def billable + "billable_#{params[:billable]}" if params[:billable].present? + end - def ticket - "ticket_#{params[:ticket]}" if params[:ticket].present? + def ticket + "ticket_#{params[:ticket]}" if params[:ticket].present? + end end end end diff --git a/app/domain/plannings/board.rb b/app/domain/plannings/board.rb index dbb89ecd2..38386d30f 100644 --- a/app/domain/plannings/board.rb +++ b/app/domain/plannings/board.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -89,10 +91,10 @@ def load_data end def load_employments - list = Employment. - during(@period). - where(employee_id: @employees.map(&:id)). - reorder('start_date') + list = Employment + .during(@period) + .where(employee_id: @employees.map(&:id)) + .reorder('start_date') Employment.normalize_boundaries(list.to_a, @period) end @@ -145,7 +147,7 @@ def add_employments_to_rows(rows) def add_holidays_to_rows(rows) @holidays.each do |holiday| - rows.each do |_key, items| + rows.each_value do |items| index = item_index(holiday[0]) next unless index diff --git a/app/domain/plannings/company_overview.rb b/app/domain/plannings/company_overview.rb index 454a814f0..7de803302 100644 --- a/app/domain/plannings/company_overview.rb +++ b/app/domain/plannings/company_overview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/domain/plannings/creator.rb b/app/domain/plannings/creator.rb index 1bd66df1f..70e691590 100644 --- a/app/domain/plannings/creator.rb +++ b/app/domain/plannings/creator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -7,9 +9,9 @@ module Plannings class Creator attr_reader :params, :errors, :plannings - PERMITTED_ATTRIBUTES = [:id, :employee_id, :work_item_id, :date, :percent, :definitive, - :translate_by].freeze - ITEM_FIELDS = [:employee_id, :work_item_id, :date].freeze + PERMITTED_ATTRIBUTES = %i[id employee_id work_item_id date percent definitive + translate_by].freeze + ITEM_FIELDS = %i[employee_id work_item_id date].freeze # params: # { planning: { percent: 50, definitive: true, repeat_until: '2016 42', translate_by: -3 }, @@ -26,10 +28,10 @@ def create_or_update @plannings = [] unless repeat_only? - @plannings = @plannings.concat(create) - @plannings = @plannings.concat(update) + @plannings.concat(create) + @plannings.concat(update) end - @plannings = @plannings.concat(repeat) if repeat_until_week + @plannings.concat(repeat) if repeat_until_week @plannings.uniq! @errors.blank? @@ -58,26 +60,24 @@ def repeat_only? private def validate_create(p) - if create? && !repeat_only? - if p[:percent].blank? - @errors << 'Prozent müssen angegeben werden, um neue Planungen zu erstellen' - end - if p[:definitive].blank? && p[:definitive] != false - @errors << 'Status muss angegeben werden, um neue Planungen zu erstellen' - end - end + return unless create? && !repeat_only? + + @errors << 'Prozent müssen angegeben werden, um neue Planungen zu erstellen' if p[:percent].blank? + return unless p[:definitive].blank? && p[:definitive] != false + + @errors << 'Status muss angegeben werden, um neue Planungen zu erstellen' end def validate_percent(p) - if p[:percent].present? && p[:percent].to_i <= 0 - @errors << 'Prozent müssen grösser als 0 sein' - end + return unless p[:percent].present? && p[:percent].to_i <= 0 + + @errors << 'Prozent müssen grösser als 0 sein' end def validate_repeat(_p) - if repeat_until_week && !repeat_until_week.valid? - @errors << 'Wiederholungsdatum ist ungültig' - end + return unless repeat_until_week && !repeat_until_week.valid? + + @errors << 'Wiederholungsdatum ist ungültig' end def validate_present(p) @@ -91,15 +91,15 @@ def validate_present(p) end def validate_work_items(_p) - if create? - work_item_ids = new_items_hashes.map { |item| item['work_item_id'] }.compact.uniq - return if work_item_ids.blank? + return unless create? - items = WorkItem.joins(:accounting_post).where(id: work_item_ids) - unless work_item_ids.length == items.count - @errors << 'Nur Positionen mit Buchungsposition sind möglich' - end - end + work_item_ids = new_items_hashes.pluck('work_item_id').compact.uniq + return if work_item_ids.blank? + + items = WorkItem.joins(:accounting_post).where(id: work_item_ids) + return if work_item_ids.length == items.count + + @errors << 'Nur Positionen mit Buchungsposition sind möglich' end def create? @@ -150,7 +150,7 @@ def translate_plannings Planning.where( employee_id: item.employee_id, work_item_id: item.work_item_id, - date: date + date: ).delete_all item.save! item @@ -159,7 +159,7 @@ def translate_plannings def translate_date(date, translate_by) translate_by = translate_by.to_i - direction = translate_by < 0 ? -1 : 1 + direction = translate_by.negative? ? -1 : 1 translate_by.abs.times do date += direction.day date += direction.day if date.saturday? || date.sunday? @@ -183,7 +183,7 @@ def repeat_plannings(offset, end_date) p = Planning.where(employee_id: planning.employee_id, work_item_id: planning.work_item_id, - date: date).first_or_initialize + date:).first_or_initialize p.percent = planning.percent p.definitive = planning.definitive p.save! @@ -212,11 +212,11 @@ def convert_to_parameters(value) def handle_save_errors(plannings) save_errors = plannings.map { |p| p.errors.full_messages }.flatten.compact - if save_errors.present? - # should not happen after form validations - @errors << 'Eintrag konnte nicht erstellt werden: ' + save_errors.uniq.join(', ') - fail ActiveRecord::Rollback - end + return if save_errors.blank? + + # should not happen after form validations + @errors << ("Eintrag konnte nicht erstellt werden: #{save_errors.uniq.join(', ')}") + raise ActiveRecord::Rollback end def new_items_hashes @@ -227,9 +227,9 @@ def new_items_hashes def existing_items_hashes @existing_items_hashes ||= existing_items.pluck(*ITEM_FIELDS).map do |values| - { 'employee_id' => values.first.to_s, + { 'employee_id' => values.first.to_s, 'work_item_id' => values.second.to_s, - 'date' => values.third.strftime('%Y-%m-%d') } + 'date' => values.third.strftime('%Y-%m-%d') } end end diff --git a/app/domain/plannings/employee_board.rb b/app/domain/plannings/employee_board.rb index 7020fff2c..f262200b2 100644 --- a/app/domain/plannings/employee_board.rb +++ b/app/domain/plannings/employee_board.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -122,26 +124,26 @@ def compute_weekly_employment_percent(date) nil end - def percent_for_employment(date, employment, i, &block) + def percent_for_employment(date, employment, i, &) period = employment.period if period.include?(date) && period.include?(date + 4) # employment covers entire week - employment_percent_during(employment, date, date + 4, &block) + employment_percent_during(employment, date, date + 4, &) elsif period.include?(date) # employment changes in the middle of the week # we assume max one employment change per week - percent_for_multiple_employments(employment, date, i, &block) + percent_for_multiple_employments(employment, date, i, &) elsif period.start_date > date && period.include?(date + 4) # first employment starts in the middle of the week - employment_percent_during(employment, employment.start_date, date + 4, &block) + employment_percent_during(employment, employment.start_date, date + 4, &) end end - def percent_for_multiple_employments(employment, date, i, &block) - percent = employment_percent_during(employment, date, employment.period.end_date, &block) + def percent_for_multiple_employments(employment, date, i, &) + percent = employment_percent_during(employment, date, employment.period.end_date, &) next_employment = @employments[i + 1] if next_employment && next_employment.start_date <= date + 4 - percent += employment_percent_during(next_employment, next_employment.start_date, date + 4, &block) + percent += employment_percent_during(next_employment, next_employment.start_date, date + 4, &) end percent end diff --git a/app/domain/plannings/item.rb b/app/domain/plannings/item.rb index d6f5f0211..f5edbce12 100644 --- a/app/domain/plannings/item.rb +++ b/app/domain/plannings/item.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -35,7 +37,7 @@ def title if absencetimes.present? absencetimes.map { |a| "Abwesenheit: #{a.hours} h" }.join("\n") elsif holiday - if holiday[1] > 0 + if holiday[1].positive? "Feiertag: #{holiday[1]} Muss Stunden" else 'Feiertag: Keine muss Stunden' @@ -56,17 +58,11 @@ def class_name class_names << "-percent-#{planning.percent.round(-1)}" end - if absencetimes.present? - class_names << '-absence' - end + class_names << '-absence' if absencetimes.present? - if employment.nil? || employment.percent.zero? - class_names << '-absence-unpaid' - end + class_names << '-absence-unpaid' if employment.nil? || employment.percent.zero? - if holiday - class_names << '-holiday' - end + class_names << '-holiday' if holiday class_names.join(' ') end diff --git a/app/domain/plannings/order_board.rb b/app/domain/plannings/order_board.rb index 80e4ba400..31abba988 100644 --- a/app/domain/plannings/order_board.rb +++ b/app/domain/plannings/order_board.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -49,26 +51,26 @@ def included_accounting_posts private def load_plannings(p = period) - super(p). - joins(:work_item). - where('? = ANY (work_items.path_ids)', order.work_item_id) + super(p) + .joins(:work_item) + .where('? = ANY (work_items.path_ids)', order.work_item_id) end def load_accounting_posts - order.accounting_posts. - where(closed: false). - includes(:work_item). - list + order.accounting_posts + .where(closed: false) + .includes(:work_item) + .list end def load_total_included_rows_planned_hours hours = {} WorkingCondition.each_period_of(:must_hours_per_day, Period.new(nil, nil)) do |period, val| - load_plannings(period). - where(included_plannings_condition). - group(:employee_id, :work_item_id). - sum(:percent). - each do |(e, w), p| + load_plannings(period) + .where(included_plannings_condition) + .group(:employee_id, :work_item_id) + .sum(:percent) + .each do |(e, w), p| hours[key(e, w)] ||= 0 hours[key(e, w)] += percent_to_hours(p, val) end @@ -79,11 +81,11 @@ def load_total_included_rows_planned_hours def load_total_posts_planned_hours hours = {} WorkingCondition.each_period_of(:must_hours_per_day, Period.new(nil, nil)) do |period, val| - load_plannings(period). - where(work_item_id: included_work_item_ids). - group(:work_item_id). - sum(:percent). - each do |w, p| + load_plannings(period) + .where(work_item_id: included_work_item_ids) + .group(:work_item_id) + .sum(:percent) + .each do |w, p| hours[w] ||= 0 hours[w] += percent_to_hours(p, val) end diff --git a/app/domain/presenters/log_presenter.rb b/app/domain/presenters/log_presenter.rb index a7ca3d0a1..9826a4a67 100644 --- a/app/domain/presenters/log_presenter.rb +++ b/app/domain/presenters/log_presenter.rb @@ -5,118 +5,127 @@ # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class LogPresenter - attr_accessor :version - attr_reader :employee, :params, :view - - def initialize(employee, params = nil, view = nil) - @employee = employee - @params = params - @view = view - end +module Presenters + class LogPresenter + attr_accessor :version + attr_reader :employee, :params, :view + + def initialize(employee, params = nil, view = nil) + @employee = employee + @params = params + @view = view + end - def versions - @versions ||= - PaperTrail::Version.where(employee_id: employee.id) - .or(legacy_employee_log) - .or(legacy_employment_log) - .reorder('created_at DESC, id DESC') - .includes(:item) - .page(params[:page]) - end + def versions + @versions ||= + PaperTrail::Version.where(employee_id: employee.id) + .or(legacy_employee_log) + .or(legacy_employment_log) + .reorder('created_at DESC, id DESC') + .includes(:item) + .page(params[:page]) + end - def present_author(versions) - employee = Employee.find_by(id: versions.first.version_author) - yield employee.to_s if employee - end + def present_author(versions) + employee = Employee.find_by(id: versions.first.version_author) + yield employee.to_s if employee + end - def present_changes(versions) - versions.each do |version| - yield title_for(version), changes_for(version) + def present_changes(versions) + versions.each do |version| + yield title_for(version), changes_for(version) + end end - end - def title_for(version) - model = version.item_type.parameterize - event = version.event - I18n.t("version.model.#{event}.#{model}", id: version.item_id) - end + def title_for(version) + model = version.item_type.parameterize + return send(:"title_for_#{model}", version) if respond_to?(:"title_for_#{model}") - def changes_for(version) - version.changeset.collect do |attr, changes| - next if changes.all?(&:blank?) + event = version.event + I18n.t("version.model.#{event}.#{model}", id: version.item_id) + end - attribute_change(version.item_type, attr, changes) + def title_for_employmentrolesemployment(version) + entry_class = version.item_type.constantize + entry = entry_class.find_by(id: version.item_id) || (version.object && entry_class.new(version.object_deserialized)) + role = entry&.employment_role || '(deleted)' + I18n.t("version.model.#{version.event}.employmentrolesemployment", role:, employment_id: entry&.employment_id) end - end - def attribute_change(item_type, attr, changes) - from, to = changes - key = attribute_key(from, to) + def changes_for(version) + version.changeset.collect do |attr, changes| + next if changes.all?(&:blank?) - if attr == 'committed_worktimes_at' - from = I18n.l(from, format: :month) if from - to = I18n.l(to, format: :month) if to + attribute_change(version.item_type, attr, changes) + end end - I18n.t("version.attribute_change.#{key}", attribute_args(item_type, attr, from, to)) - end - - private + def attribute_change(item_type, attr, changes) + from, to = changes + key = attribute_key(from, to) - def legacy_employee_log - PaperTrail::Version.where( - item_id: employee.id, - item_type: Employee.sti_name, - employee_id: nil - ) - end + if attr == 'committed_worktimes_at' + from = I18n.l(from, format: :month) if from + to = I18n.l(to, format: :month) if to + end - def legacy_employment_log - PaperTrail::Version.where(employee_id: nil).where(legacy_employment_query) - end + I18n.t("version.attribute_change.#{key}", **attribute_args(item_type, attr, from, to)) + end - def legacy_employment_query - id = employee.id + private - # find created, updated and destroyed models - ["- \n- #{id}\n", "- #{id}\n- #{id}\n", "- #{id}\n- \n"] - .collect { |s| "object_changes LIKE '%employee_id:\n#{s}%'" } - .join(' OR ') - end + def legacy_employee_log + PaperTrail::Version.where( + item_id: employee.id, + item_type: Employee.sti_name, + employee_id: nil + ) + end - def attribute_key(from, to) - if from.present? && to.present? - 'from_to' - elsif from.present? - 'from' - elsif to.present? - 'to' + def legacy_employment_log + PaperTrail::Version.where(employee_id: nil).where(legacy_employment_query) end - end - def attribute_args(item_type, attr, from, to) - item_class = item_type.constantize - attr_s = attr.to_s - if item_class.defined_enums[attr_s] - to = item_class.human_attribute_name([attr_s.pluralize, to].join('.')) + def legacy_employment_query + id = employee.id + + # find created, updated and destroyed models + ["- \n- #{id}\n", "- #{id}\n- #{id}\n", "- #{id}\n- \n"] + .collect { |s| "object_changes LIKE '%employee_id:\n#{s}%'" } + .join(' OR ') end - association = attr.gsub(/_id\z/, '') - if reflection = item_class.reflect_on_association(association) - from = resolve_association(reflection, from) - to = resolve_association(reflection, to) + def attribute_key(from, to) + if from.present? && to.present? + 'from_to' + elsif from.present? + 'from' + elsif to.present? + 'to' + end end - { - attr: item_class.human_attribute_name(attr), - model_ref: I18n.t("version.model_reference.#{item_class.name.parameterize}"), - from: from, - to: to - } - end + def attribute_args(item_type, attr, from, to) + item_class = item_type.constantize + attr_s = attr.to_s + to = item_class.human_attribute_name([attr_s.pluralize, to].join('.')) if item_class.defined_enums[attr_s] + + association = attr.delete_suffix('_id') + if (reflection = item_class.reflect_on_association(association)) + from = resolve_association(reflection, from) + to = resolve_association(reflection, to) + end + + { + attr: item_class.human_attribute_name(attr), + model_ref: I18n.t("version.model_reference.#{item_class.name.parameterize}"), + from:, + to: + } + end - def resolve_association(reflection, id) - reflection.class_name.constantize.find_by(id: id) || id + def resolve_association(reflection, id) + reflection.class_name.constantize.find_by(id:) || id + end end end diff --git a/app/domain/rate_limiter.rb b/app/domain/rate_limiter.rb index 96c727868..7114af2e1 100644 --- a/app/domain/rate_limiter.rb +++ b/app/domain/rate_limiter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/domain/reports/bi_workload.rb b/app/domain/reports/bi_workload.rb deleted file mode 100644 index 6846865b8..000000000 --- a/app/domain/reports/bi_workload.rb +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -class Reports::BIWorkload - PROPERTIES = %i[ - employment_fte - must_hours - ordertime_hours - paid_absence_hours - worktime_balance - external_client_hours - billable_hours - workload - billability - absolute_billability - ] - - def initialize(today = DateTime.now) - @today = today - end - - def stats - periods.flat_map do |period, period_tags| - departments.flat_map do |department| - department_stats(department, period, period_tags) - end - end - end - - private - - def periods - last_week = @today - 1.week - last_month = @today - 1.month - - [ - [ - Period.new(last_week.beginning_of_week, last_week.end_of_week), - { week: last_week.strftime('CW %-V') } - ], - [ - Period.new(last_month.beginning_of_month, last_month.end_of_month), - { month: last_month.strftime('%Y-%m') } - ] - ] - end - - def departments - Department.having_employees - end - - def make_period; end - - def department_stats(department, period, tags) - _company, department = Reports::Workload.new(period, department).summary - - fields = - PROPERTIES.each_with_object({}) do |prop, memo| - memo[prop] = department.send(prop) - end - - { - name: 'workload', - fields: fields, - tags: { department: department.label.to_s }.merge(tags) - } - end -end diff --git a/app/domain/reports/extended_capacity_report.rb b/app/domain/reports/extended_capacity_report.rb index 3e08e1cc5..709aabf73 100644 --- a/app/domain/reports/extended_capacity_report.rb +++ b/app/domain/reports/extended_capacity_report.rb @@ -1,228 +1,234 @@ -# -*- coding: utf-8 -*- +# frozen_string_literal: true # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class ExtendedCapacityReport - def initialize(current_period) - @period = current_period - end +module Reports + class ExtendedCapacityReport + def initialize(current_period) + @period = current_period + end - def to_csv - CSV.generate do |csv| - add_header(csv) - add_employees(csv) + def to_csv + CSV.generate do |csv| + add_header(csv) + add_employees(csv) + end end - end - def filename - "puzzletime_detaillierte_auslastung_#{format_date(@period.start_date)}_#{format_date(@period.end_date)}.csv" - end + def filename + "puzzletime_detaillierte_auslastung_#{format_date(@period.start_date)}_#{format_date(@period.end_date)}.csv" + end - private - - def add_header(csv) - csv << ['Member', - 'Auftrag Organisationseinheit', - 'Beschäftigungsgrad (%)', - 'Soll Arbeitszeit (h)', - 'Überstunden (h)', - 'Überstunden Total (h)', - "Ferienguthaben bis Ende #{@period.end_date.year} (d)", - 'Abwesenheit (h)', - 'Projektkürzel', - 'Projektname', - 'Subprojektname', - 'Projekte Total (h)', - 'Projekte Total - Detail (h)', - 'Stundensatz', - 'Kunden-Projekte Total (h)', - 'Kunden-Projekte Total - Detail (h)', - 'Kunden-Projekte Total verrechenbar (h)', - 'Kunden-Projekte Total verrechenbar - Detail (h)', - 'Kunden-Projekte Total nicht verrechenbar (h)', - 'Kunden-Projekte Total nicht verrechenbar - Detail (h)', - 'Interne Projekte Total (h)', - 'Interne Projekte Total - Detail (h)'] - end + private + + def add_header(csv) + csv << ['Member', + 'Auftrag Organisationseinheit', + 'Beschäftigungsgrad (%)', + 'Soll Arbeitszeit (h)', + 'Überstunden (h)', + 'Überstunden Total (h)', + "Ferienguthaben bis Ende #{@period.end_date.year} (d)", + 'Abwesenheit (h)', + 'Projektkürzel', + 'Projektname', + 'Subprojektname', + 'Projekte Total (h)', + 'Projekte Total - Detail (h)', + 'Ist bereinigt ver. Rollenanteile (h)', + 'Stundensatz', + 'Kunden-Projekte Total (h)', + 'Kunden-Projekte Total - Detail (h)', + 'Kunden-Projekte Total verrechenbar (h)', + 'Kunden-Projekte Total verrechenbar - Detail (h)', + 'Kunden-Projekte Total nicht verrechenbar (h)', + 'Kunden-Projekte Total nicht verrechenbar - Detail (h)', + 'Interne Projekte Total (h)', + 'Interne Projekte Total - Detail (h)'] + end - def add_employees(csv) - Employee.employed_ones(@period).each do |employee| - work_items = { billable: [], non_billable: [], internal: [] } - employee.alltime_leaf_work_items.each do |work_item| - if internal?(work_item) - work_items[:internal] << work_item - elsif work_item.accounting_post.billable - work_items[:billable] << work_item - else - work_items[:non_billable] << work_item + def add_employees(csv) + Employee.employed_ones(@period).each do |employee| + work_items = { billable: [], non_billable: [], internal: [] } + employee.alltime_leaf_work_items.each do |work_item| + if internal?(work_item) + work_items[:internal] << work_item + elsif work_item.accounting_post.billable + work_items[:billable] << work_item + else + work_items[:non_billable] << work_item + end end - end - customer_rows = employee_customer_rows(employee, work_items[:billable] + work_items[:non_billable]) - internal_rows = employee_internal_rows(employee, work_items[:internal]) - all_rows = customer_rows + internal_rows + customer_rows = employee_customer_rows(employee, work_items[:billable] + work_items[:non_billable]) + internal_rows = employee_internal_rows(employee, work_items[:internal]) + all_rows = customer_rows + internal_rows - csv << employee_summary_row(employee, all_rows) - all_rows.each { |row| csv << row } + csv << employee_summary_row(employee, all_rows) + all_rows.each { |row| csv << row } + end end - end - def employee_summary_row(employee, rows) - [employee.shortname, - '', - employee_average_percents(employee), - employee.statistics.musttime(@period), - employee.statistics.overtime(@period), - employee.statistics.current_overtime(@period.end_date), - employee_remaining_vacations(employee), - employee_absences(employee, @period), - '', - '', - '', - rows.map { |r| r[12].to_f }.sum, # Projekte Total (h) - '', - '', - rows.map { |r| r[15].to_f }.sum, # Kunden-Projekte Total (h) - '', - rows.map { |r| r[17].to_f }.sum, # Kunden-Projekte Total verrechenbar (h) - '', - rows.map { |r| r[19].to_f }.sum, # Kunden-Projekte Total nicht verrechenbar (h) - '', - rows.map { |r| r[21].to_f }.sum, # Interne Projekte Total (h) - ''] - end + def employee_summary_row(employee, rows) + total_percents_at_start = employee.statistics.percents_at(@period.start_date) + billable_percents_at_start = employee.statistics.billable_percents_at(@period.start_date) + projekte_total_h = rows.sum { |r| r[12].to_f } + projekte_total_h_bereinigt = projekte_total_h * billable_percents_at_start / total_percents_at_start + + [employee.shortname, + '', + employee.statistics.average_percents(@period), + employee.statistics.musttime(@period), + employee.statistics.overtime(@period), + employee.statistics.current_overtime(@period.end_date), + employee_remaining_vacations(employee), + employee_absences(employee, @period), + '', + '', + '', + projekte_total_h, # Projekte Total (h) + '', + projekte_total_h_bereinigt, # Ist bereinigt ver. Rollenanteile (h) + '', + rows.sum { |r| r[16].to_f }, # Kunden-Projekte Total (h) + '', + rows.sum { |r| r[18].to_f }, # Kunden-Projekte Total verrechenbar (h) + '', + rows.sum { |r| r[20].to_f }, # Kunden-Projekte Total nicht verrechenbar (h) + '', + rows.sum { |r| r[22].to_f }, # Interne Projekte Total (h) + ''] + end - def employee_customer_rows(employee, work_items) - rows = [] - work_items.each do |work_item| - times = find_billable_time(employee, work_item.id, @period) + def employee_customer_rows(employee, work_items) + rows = [] + work_items.each do |work_item| + times = find_billable_time(employee, work_item.id, @period) - billable_hours = extract_billable_hours(times, true) - non_billable_hours = extract_billable_hours(times, false) + billable_hours = extract_billable_hours(times, true) + non_billable_hours = extract_billable_hours(times, false) - next unless (billable_hours + non_billable_hours).abs > 0.001 + next unless (billable_hours + non_billable_hours).abs > 0.001 - rows << build_employee_row(employee, work_item, - billable_hours: billable_hours, - non_billable_hours: non_billable_hours) + rows << build_employee_row(employee, work_item, + billable_hours:, + non_billable_hours:) + end + rows end - rows - end - def employee_internal_rows(employee, work_items) - rows = [] - work_items.each do |work_item| - times = find_billable_time(employee, work_item.id, @period) + def employee_internal_rows(employee, work_items) + rows = [] + work_items.each do |work_item| + times = find_billable_time(employee, work_item.id, @period) - internal_hours = extract_billable_hours(times, false) + - extract_billable_hours(times, true) + internal_hours = extract_billable_hours(times, false) + + extract_billable_hours(times, true) - next unless internal_hours.abs > 0.001 + next unless internal_hours.abs > 0.001 - rows << build_employee_row(employee, work_item, - internal_hours: internal_hours) + rows << build_employee_row(employee, work_item, + internal_hours:) + end + rows end - rows - end - def build_employee_row(employee, work_item, data = {}) - parent = child = work_item - parent = child.parent if child.parent - - [employee.shortname, - work_item_department(work_item), - '', - '', - '', - '', - '', - '', - work_item_code(parent, child), - work_item_label(parent), - subwork_item_label(parent, child), - '', - employee_overall_total(data), - offered_rate(work_item), - '', - employee_customer_total(data), - '', - data.fetch(:billable_hours, ''), - '', - data.fetch(:non_billable_hours, ''), - '', - data.fetch(:internal_hours, '')] - end - - def find_billable_time(employee, work_item_id, period) - Worktime.find_by_sql(['SELECT SUM(w.hours) AS HOURS, w.billable FROM worktimes w ' \ - 'LEFT JOIN work_items p ON p.id = w.work_item_id ' \ - 'WHERE w.employee_id = ? AND ? = ANY(p.path_ids) ' \ - 'AND w.work_date BETWEEN ? AND ? ' \ - 'GROUP BY w.billable', - employee.id, work_item_id, period.start_date, period.end_date]) - end + def build_employee_row(employee, work_item, data = {}) + parent = child = work_item + parent = child.parent if child.parent + + [employee.shortname, + work_item_department(work_item), + '', + '', + '', + '', + '', + '', + work_item_code(parent, child), + work_item_label(parent), + subwork_item_label(parent, child), + '', + employee_overall_total(data), + '', + offered_rate(work_item), + '', + employee_customer_total(data), + '', + data.fetch(:billable_hours, ''), + '', + data.fetch(:non_billable_hours, ''), + '', + data.fetch(:internal_hours, '')] + end - def employee_absences(employee, period) - employee.worktimes.includes(:absence) - .in_period(period) - .where(type: 'Absencetime', absences: { payed: true }) - .sum(:hours) - .to_f - end + def find_billable_time(employee, work_item_id, period) + Worktime.find_by_sql(['SELECT SUM(w.hours) AS HOURS, w.billable FROM worktimes w ' \ + 'LEFT JOIN work_items p ON p.id = w.work_item_id ' \ + 'WHERE w.employee_id = ? AND ? = ANY(p.path_ids) ' \ + 'AND w.work_date BETWEEN ? AND ? ' \ + 'GROUP BY w.billable', + employee.id, work_item_id, period.start_date, period.end_date]) + end - def extract_billable_hours(result, billable) - entry = result.find { |w| w.billable == billable } - entry ? entry.hours : 0 - end + def employee_absences(employee, period) + employee.worktimes.includes(:absence) + .in_period(period) + .where(type: 'Absencetime', absences: { payed: true }) + .sum(:hours) + .to_f + end - def internal?(work_item) - Array.wrap(work_item.path_ids).include?(Company.work_item_id) - end + def extract_billable_hours(result, billable) + entry = result.find { |w| w.billable == billable } + entry ? entry.hours : 0 + end - def employee_average_percents(employee) - employee.statistics.employments_during(@period).sum(&:percent) - end + def internal?(work_item) + Array.wrap(work_item.path_ids).include?(Company.work_item_id) + end - def employee_remaining_vacations(employee) - employee.statistics.remaining_vacations(Date.new(@period.end_date.year, 12, 31)) - end + def employee_remaining_vacations(employee) + employee.statistics.remaining_vacations(Date.new(@period.end_date.year, 12, 31)) + end - def work_item_code(_work_item, subwork_item) - subwork_item.path_shortnames - end + def work_item_code(_work_item, subwork_item) + subwork_item.path_shortnames + end - def work_item_label(work_item) - work_item.label_verbose - end + def work_item_label(work_item) + work_item.label_verbose + end - def offered_rate(work_item) - work_item.accounting_post.offered_rate - end + def offered_rate(work_item) + work_item.accounting_post.offered_rate + end - def work_item_department(work_item) - work_item.accounting_post.order.department - end + def work_item_department(work_item) + work_item.accounting_post.order.department + end - def subwork_item_label(work_item, subwork_item) - subwork_item == work_item ? '' : subwork_item.label - end + def subwork_item_label(work_item, subwork_item) + subwork_item == work_item ? '' : subwork_item.label + end - def employee_overall_total(data) - data.fetch(:billable_hours, 0) + data.fetch(:non_billable_hours, 0) + data.fetch(:internal_hours, 0) - end + def employee_overall_total(data) + data.fetch(:billable_hours, 0) + data.fetch(:non_billable_hours, 0) + data.fetch(:internal_hours, 0) + end - def employee_customer_total(data) - if data[:billable_hours].present? || data[:non_billable_hourse].present? - data.fetch(:billable_hours, 0) + data.fetch(:non_billable_hours, 0) - else - '' + def employee_customer_total(data) + if data[:billable_hours].present? || data[:non_billable_hourse].present? + data.fetch(:billable_hours, 0) + data.fetch(:non_billable_hours, 0) + else + '' + end end - end - def format_date(date) - I18n.l(date, format: '%Y%m%d') + def format_date(date) + I18n.l(date, format: '%Y%m%d') + end end end diff --git a/app/domain/reports/overtime_vacations_report.rb b/app/domain/reports/overtime_vacations_report.rb index 969cafce2..baea900c2 100644 --- a/app/domain/reports/overtime_vacations_report.rb +++ b/app/domain/reports/overtime_vacations_report.rb @@ -1,133 +1,140 @@ -class OvertimeVacationsReport - def initialize(date) - @date = date - @filename_prefix = 'puzzletime_überzeit_ferien' - end +# frozen_string_literal: true - def filename - "#{@filename_prefix}_#{format_date_short(@date)}.csv" - end +module Reports + class OvertimeVacationsReport + def initialize(date) + @date = date + @filename_prefix = 'puzzletime_überzeit_ferien' + end - def to_csv - CSV.generate do |csv| - add_header(csv) - add_employees(csv) + def filename + "#{@filename_prefix}_#{format_date_short(@date)}.csv" end - end - private + def to_csv + CSV.generate do |csv| + add_header(csv) + add_employees(csv) + end + end - def period - Period.day_for(@date) - # @period ||= Period.new( - # @employee.employments.minimum(:start_date), - # @date - # ) - end + private - def add_header(csv) - header = [ - Employee.model_name.human, - 'Überzeit bis Stichdatum', - 'Ferienguthaben bis Stichdatum', - 'Pensum' - ] - csv << ["Überzeit/Ferien per #{format_date_long(@date)}, #{format_business_year(@date)}"] + Array.new(header.length - 1, '') - csv << header - end + def period + Period.day_for(@date) + # @period ||= Period.new( + # @employee.employments.minimum(:start_date), + # @date + # ) + end - def add_employees(csv) - @totals = {} - groups = employees.map { |e| [e.department_id, e.department_name] }.uniq - groups.each do |department_id, department_name| - add_department(csv, department_name) - employees.select { |e| e.department_id == department_id } - .each { |e| add_employee(csv, e) } - add_department_totals(csv, department_id, department_name) + def add_header(csv) + header = [ + Employee.model_name.human, + 'Überzeit bis Stichdatum', + 'Ferienguthaben bis Stichdatum', + 'Pensum' + ] + csv << (["Überzeit/Ferien per #{format_date_long(@date)}, #{format_business_year(@date)}"] + Array.new( + header.length - 1, '' + )) + csv << header end - add_overall_totals(csv) - end - def add_department(csv, name) - add_empty(csv) - csv << ["#{Department.model_name.human} #{name}"] + Array.new(3, '') - end + def add_employees(csv) + @totals = {} + groups = employees.map { |e| [e.department_id, e.department_name] }.uniq + groups.each do |department_id, department_name| + add_department(csv, department_name) + employees.select { |e| e.department_id == department_id } + .each { |e| add_employee(csv, e) } + add_department_totals(csv, department_id, department_name) + end + add_overall_totals(csv) + end - def add_employee(csv, employee) - csv << [ - employee.to_s, - employee.statistics.current_overtime(@date), - employee.statistics.remaining_vacations(@date), - format_percent(employee.current_percent_value) - ] - sum_up_employee(employee) - end + def add_department(csv, name) + add_empty(csv) + csv << (["#{Department.model_name.human} #{name}"] + Array.new(3, '')) + end - def add_department_totals(csv, id, name) - totals = @totals[id] - add_empty(csv) - csv << [ - "Total #{name}", - totals[:current_overtime], - totals[:remaining_vacations], - format_percent(totals[:current_percent_value]) - ] - end + def add_employee(csv, employee) + csv << [ + employee.to_s, + employee.statistics.current_overtime(@date), + employee.statistics.remaining_vacations(@date), + format_percent(employee.current_percent_value) + ] + sum_up_employee(employee) + end - def add_overall_totals(csv) - add_empty(csv) - csv << ["Überzeit/Ferien per #{format_date_long(@date)}, #{format_business_year(@date)}"] + Array.new(3, '') - csv << ['', 'Überzeit bis Stichdatum', 'Ferienguthaben bis Stichdatum', 'Pensum'] - csv << [ - 'Total', - overall_total(:current_overtime), - overall_total(:remaining_vacations), - format_percent(overall_total(:current_percent_value)) - ] - end + def add_department_totals(csv, id, name) + totals = @totals[id] + add_empty(csv) + csv << [ + "Total #{name}", + totals[:current_overtime], + totals[:remaining_vacations], + format_percent(totals[:current_percent_value]) + ] + end - def add_empty(csv, length = 3) - csv << Array.new(length, '') - end + def add_overall_totals(csv) + add_empty(csv) + csv << (["Überzeit/Ferien per #{format_date_long(@date)}, #{format_business_year(@date)}"] + Array.new(3, '')) + csv << ['', 'Überzeit bis Stichdatum', 'Ferienguthaben bis Stichdatum', 'Pensum'] + csv << [ + 'Total', + overall_total(:current_overtime), + overall_total(:remaining_vacations), + format_percent(overall_total(:current_percent_value)) + ] + end - def sum_up_employee(employee) - totals = @totals[employee.department_id] ||= {} - totals[:current_overtime] = totals[:current_overtime].to_f + employee.statistics.current_overtime(@date).to_f - totals[:remaining_vacations] = totals[:remaining_vacations].to_f + employee.statistics.remaining_vacations(@date).to_f - totals[:current_percent_value] = totals[:current_percent_value].to_f + employee.current_percent_value.to_f - end + def add_empty(csv, length = 3) + csv << Array.new(length, '') + end - def overall_total(attr) - @totals.sum { |_k, v| v[attr] } - end + def sum_up_employee(employee) + totals = @totals[employee.department_id] ||= {} + totals[:current_overtime] = totals[:current_overtime].to_f + employee.statistics.current_overtime(@date).to_f + totals[:remaining_vacations] = + totals[:remaining_vacations].to_f + employee.statistics.remaining_vacations(@date).to_f + totals[:current_percent_value] = totals[:current_percent_value].to_f + employee.current_percent_value.to_f + end - def employees - @employees ||= - Employee - .select( - :id, :lastname, :firstname, :initial_vacation_days, - :department_id, 'departments.name AS department_name', - 'em.percent AS current_percent_value' - ) - .employed_ones(period) - .joins(:department) - .reorder(:department_name, :lastname, :firstname) - end + def overall_total(attr) + @totals.sum { |_k, v| v[attr] } + end - def format_date_short(date) - I18n.l(date, format: '%Y%m%d') - end + def employees + @employees ||= + Employee + .select( + :id, :lastname, :firstname, :initial_vacation_days, + :department_id, 'departments.name AS department_name', + 'em.percent AS current_percent_value' + ) + .employed_ones(period) + .joins(:department) + .reorder(:department_name, :lastname, :firstname) + end - def format_date_long(date) - I18n.l(date, format: '%d.%m.%Y') - end + def format_date_short(date) + I18n.l(date, format: '%Y%m%d') + end - def format_business_year(date) - period = Period.business_year_for(date) - "GJ #{[period.start_date.year, period.end_date.year].uniq.join('/')}" - end + def format_date_long(date) + I18n.l(date, format: '%d.%m.%Y') + end + + def format_business_year(date) + period = Period.business_year_for(date) + "GJ #{[period.start_date.year, period.end_date.year].uniq.join('/')}" + end - def format_percent(value) - value.to_f.round(2).to_s + '%' + def format_percent(value) + "#{value.to_f.round(2)}%" + end end end diff --git a/app/domain/reports/revenue/base.rb b/app/domain/reports/revenue/base.rb index 37d1f09f0..340fbe42a 100644 --- a/app/domain/reports/revenue/base.rb +++ b/app/domain/reports/revenue/base.rb @@ -1,244 +1,246 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -module Reports::Revenue - class Base - class_attribute :grouping_model, :grouping_fk - delegate :grouping_name, :grouping_name_human, to: :class - attr_reader :period, :params +module Reports + module Revenue + class Base + class_attribute :grouping_model, :grouping_fk + delegate :grouping_name, :grouping_name_human, to: :class + attr_reader :period, :params - class << self - def grouping_name - grouping_model.sti_name - end + class << self + def grouping_name + grouping_model.sti_name + end - def grouping_name_human - grouping_model.model_name.human + def grouping_name_human + grouping_model.model_name.human + end end - end - def initialize(period, params = {}) - @period = period.extend_to_months - @params = params - end - - def entries - @entries ||= directed_sort(sorted(load_entries)) - end + def initialize(period, params = {}) + @period = period.extend_to_months + @params = params + end - def hours_without_entry? - ordertime_hours.any? { |(entry_id, _time), _sum_hours| entry_id.nil? } || - planning_hours.any? { |(entry_id, _date), _sum_hours| entry_id.nil? } - end + def entries + @entries ||= directed_sort(sorted(load_entries)) + end - def ordertime_hours - @ordertime_hours ||= load_ordertime_hours - end + def hours_without_entry? + ordertime_hours.any? { |(entry_id, _time), _sum_hours| entry_id.nil? } || + planning_hours.any? { |(entry_id, _date), _sum_hours| entry_id.nil? } + end - def total_ordertime_hours_per_month - @total_ordertime_hours_per_month ||= load_total_ordertime_hours_per_month - end + def ordertime_hours + @ordertime_hours ||= load_ordertime_hours + end - def total_ordertime_hours_per_entry(entry) - ordertime_hours - .select { |(entry_id, _date), _hours| entry_id == entry.try(:id) } - .values - .sum - end + def total_ordertime_hours_per_month + @total_ordertime_hours_per_month ||= load_total_ordertime_hours_per_month + end - def average_ordertime_hours_per_entry(entry) - hours = ordertime_hours - .select { |(entry_id, _date), _hours| entry_id == entry.try(:id) } - .values - hours.empty? ? 0 : hours.sum.to_f / hours.size - end + def total_ordertime_hours_per_entry(entry) + ordertime_hours + .select { |(entry_id, _date), _hours| entry_id == entry.try(:id) } + .values + .sum + end - def total_ordertime_hours_overall - ordertime_hours.values.sum - end + def average_ordertime_hours_per_entry(entry) + hours = ordertime_hours + .select { |(entry_id, _date), _hours| entry_id == entry.try(:id) } + .values + hours.empty? ? 0 : hours.sum.to_f / hours.size + end - def average_ordertime_hours_overall - hours = total_ordertime_hours_per_month.values - hours.empty? ? 0 : hours.sum.to_f / hours.size - end + def total_ordertime_hours_overall + ordertime_hours.values.sum + end - def planning_hours - @planning_hours ||= load_planning_hours - end + def average_ordertime_hours_overall + hours = total_ordertime_hours_per_month.values + hours.empty? ? 0 : hours.sum.to_f / hours.size + end - def total_planning_hours_per_month - @total_planning_hours_per_month ||= load_total_planning_hours_per_month - end + def planning_hours + @planning_hours ||= load_planning_hours + end - def past_months? - past_period.present? - end + def total_planning_hours_per_month + @total_planning_hours_per_month ||= load_total_planning_hours_per_month + end - def step_past_months - @period.step_months do |date| - yield date if date < current_month + def past_months? + past_period.present? end - end - def future_months? - future_period.present? - end + def step_past_months + @period.step_months do |date| + yield date if date < current_month + end + end - def step_future_months - @period.step_months do |date| - yield date if date >= current_month + def future_months? + future_period.present? end - end - private - - def sorted(entries) - case params[:sort] - when 'total' - entries.sort_by { |e| total_ordertime_hours_per_entry(e) } - when 'average' - entries.sort_by { |e| average_ordertime_hours_per_entry(e) } - when /^\d{4}\-\d{2}\-\d{2}$/ - sorted_month(entries) - else - entries + def step_future_months + @period.step_months do |date| + yield date if date >= current_month + end end - end - def sorted_month(entries) - date = Date.parse(params[:sort]).beginning_of_month - if date < current_month - entries.sort_by { |e| ordertime_hours[[e.id, date]] || 0 } - else - entries.sort_by { |e| planning_hours[[e.id, date]] || 0 } + private + + def sorted(entries) + case params[:sort] + when 'total' + entries.sort_by { |e| total_ordertime_hours_per_entry(e) } + when 'average' + entries.sort_by { |e| average_ordertime_hours_per_entry(e) } + when /^\d{4}-\d{2}-\d{2}$/ + sorted_month(entries) + else + entries + end end - rescue ArgumentError - entries - end - def directed_sort(entries) - if params[:sort_dir].to_s.casecmp('desc') >= 0 - entries.reverse - else + def sorted_month(entries) + date = Date.parse(params[:sort]).beginning_of_month + if date < current_month + entries.sort_by { |e| ordertime_hours[[e.id, date]] || 0 } + else + entries.sort_by { |e| planning_hours[[e.id, date]] || 0 } + end + rescue ArgumentError entries end - end - def load_entries - entry_ids = ordertime_hours - .merge(planning_hours) - .map { |(entry_id, _date), _sum| entry_id } - .uniq - grouping_model.where(id: entry_ids).list - end + def directed_sort(entries) + if params[:sort_dir].to_s.casecmp('desc') >= 0 + entries.reverse + else + entries + end + end - def load_ordertimes(period = past_period) - Ordertime - .joins(work_item: :accounting_post) - .joins('LEFT JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)') - .in_period(period) - .billable - .where.not('? = ANY (work_items.path_ids)', Settings.clients.company_id) - end + def load_entries + entry_ids = ordertime_hours + .merge(planning_hours) + .map { |(entry_id, _date), _sum| entry_id } + .uniq + grouping_model.where(id: entry_ids).list + end - def load_plannings(period) - Planning - .joins(work_item: :accounting_post) - .joins('LEFT JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)') - .in_period(period) - .definitive - .where(accounting_posts: { billable: true }) - .where.not('? = ANY (work_items.path_ids)', Settings.clients.company_id) - end + def load_ordertimes(period = past_period) + Ordertime + .joins(work_item: :accounting_post) + .joins('LEFT JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)') + .in_period(period) + .billable + .where.not('? = ANY (work_items.path_ids)', Settings.clients.company_id) + end - def load_ordertime_hours - load_ordertimes - .group(grouping_fk, work_date_grouping) - .sum('hours * offered_rate') - .each_with_object({}) do |((entry_id, time), sum_hours), result| - result[[entry_id, time.to_date]] = sum_hours + def load_plannings(period) + Planning + .joins(work_item: :accounting_post) + .joins('LEFT JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)') + .in_period(period) + .definitive + .where(accounting_posts: { billable: true }) + .where.not('? = ANY (work_items.path_ids)', Settings.clients.company_id) end - end - def load_total_ordertime_hours_per_month - load_ordertimes - .group(work_date_grouping) - .sum('hours * offered_rate') - .each_with_object({}) do |(time, sum_hours), result| - result[time.to_date] = sum_hours + def load_ordertime_hours + load_ordertimes + .group(grouping_fk, work_date_grouping) + .sum('hours * offered_rate') + .each_with_object({}) do |((entry_id, time), sum_hours), result| + result[[entry_id, time.to_date]] = sum_hours + end end - end - def load_planning_hours - load_planning_hours_each(grouping_fk, :date) do |key, sum_amount, result| - entry_id, date = key - date = date.beginning_of_month - result[[entry_id, date]] ||= 0.0 - result[[entry_id, date]] += sum_amount.to_f + def load_total_ordertime_hours_per_month + load_ordertimes + .group(work_date_grouping) + .sum('hours * offered_rate') + .transform_keys(&:to_date) end - end - def load_total_planning_hours_per_month - load_planning_hours_each(:date) do |key, sum_amount, result| - date = key.beginning_of_month - result[date] ||= 0.0 - result[date] += sum_amount.to_f + def load_planning_hours + load_planning_hours_each(grouping_fk, :date) do |key, sum_amount, result| + entry_id, date = key + date = date.beginning_of_month + result[[entry_id, date]] ||= 0.0 + result[[entry_id, date]] += sum_amount.to_f + end end - end - def load_planning_hours_each(*groupings) - result = {} - return result unless future_months? + def load_total_planning_hours_per_month + load_planning_hours_each(:date) do |key, sum_amount, result| + date = key.beginning_of_month + result[date] ||= 0.0 + result[date] += sum_amount.to_f + end + end - WorkingCondition.each_period_of(:must_hours_per_day, future_period) do |period, must_hours| - sums = load_plannings(period) - .group(groupings) - .sum("percent / 100.0 * #{must_hours.to_f} * offered_rate") - sums.each do |key, sum_amount| - yield(key, sum_amount, result) + def load_planning_hours_each(*groupings) + result = {} + return result unless future_months? + + WorkingCondition.each_period_of(:must_hours_per_day, future_period) do |period, must_hours| + sums = load_plannings(period) + .group(groupings) + .sum("percent / 100.0 * #{must_hours.to_f} * offered_rate") + sums.each do |key, sum_amount| + yield(key, sum_amount, result) + end end + result end - result - end - def work_date_grouping - 'DATE_TRUNC(\'month\', worktimes.work_date)' - end + def work_date_grouping + 'DATE_TRUNC(\'month\', worktimes.work_date)' + end - def past_period - @past_period ||= - if start_month >= current_month - nil - elsif end_month >= current_month - Period.new(@period.start_date, (current_month - 1.month).end_of_month) - else - @period - end - end + def past_period + @past_period ||= + if start_month >= current_month + nil + elsif end_month >= current_month + Period.new(@period.start_date, (current_month - 1.month).end_of_month) + else + @period + end + end - def future_period - @future_period ||= - if end_month < current_month - nil - elsif start_month < current_month - Period.new(current_month, @period.end_date) - else - @period - end - end + def future_period + @future_period ||= + if end_month < current_month + nil + elsif start_month < current_month + Period.new(current_month, @period.end_date) + else + @period + end + end - def start_month - @start_month ||= @period.start_date.beginning_of_month - end + def start_month + @start_month ||= @period.start_date.beginning_of_month + end - def end_month - @end_month ||= @period.end_date.beginning_of_month - end + def end_month + @end_month ||= @period.end_date.beginning_of_month + end - def current_month - @current_month ||= Time.zone.today.beginning_of_month + def current_month + @current_month ||= Time.zone.today.beginning_of_month + end end end end diff --git a/app/domain/reports/revenue/bi.rb b/app/domain/reports/revenue/bi.rb deleted file mode 100644 index 5d968e580..000000000 --- a/app/domain/reports/revenue/bi.rb +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -module Reports::Revenue - class BI - def initialize(window = 3.months) - @window = window - end - - def stats - now = Time.now - - report = report(now, @window) - revenue(now, report) - end - - private - - def report(time, window) - period = - Period.new( - (time - window).beginning_of_month, - (time + window).end_of_month - ) - Reports::Revenue::Department.new(period, {}) - end - - def revenue(now, report) - report.entries.each_with_object([]) do |entry, memo| - report.step_past_months do |date| - memo << revenue_stats(report, entry, date, now, :ordertime) - end - report.step_future_months do |date| - memo << revenue_stats(report, entry, date, now, :planning) - end - end.compact - end - - def revenue_stats(report, entry, date, now, source) - volume = find_volume(report, entry, date, source) - return nil if volume.nil? - - { - name: "revenue_#{source}", - fields: { volume: volume }, - tags: tags(entry, date, now) - } - end - - def tags(entry, date, now) - delta = distance_in_months(now, date) - sign = delta < 0 ? '-' : '+' - - delta_tag = "#{sign} #{delta.abs} months" - month_tag = date.strftime('%Y-%m') - - { time_delta: delta_tag, month: month_tag, department: entry.to_s } - end - - def distance_in_months(from, to) - (to.year * 12 + to.month) - (from.year * 12 + from.month) - end - - def find_volume(report, entry, date, source) - data = - source == :ordertime ? report.ordertime_hours : report.planning_hours - data[[entry.try(:id), date]] - end - end -end diff --git a/app/domain/reports/revenue/csv.rb b/app/domain/reports/revenue/csv.rb index 1cd72bca8..474be2108 100644 --- a/app/domain/reports/revenue/csv.rb +++ b/app/domain/reports/revenue/csv.rb @@ -5,138 +5,142 @@ # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Reports::Revenue::Csv - attr_reader :report, :entry +module Reports + module Revenue + class Csv + attr_reader :report, :entry - def initialize(report) - @report = report - end + def initialize(report) + @report = report + end + + def generate + CSV.generate do |csv| + csv << header - def generate - CSV.generate do |csv| - csv << header + entries.each do |e| + @entry = e + csv << row + end - entries.each do |e| - @entry = e - csv << row + csv << footer + end end - csv << footer - end - end + private - private + # Headers - # Headers + def header + [header_grouping, header_past_months, header_past_months_summary, header_future_months].flatten + end - def header - [header_grouping, header_past_months, header_past_months_summary, header_future_months].flatten - end + def header_grouping + report.grouping_name_human + end - def header_grouping - report.grouping_name_human - end + def header_past_months + out = [] + report.step_past_months { |date| out << l(date, format: :month) } + out + end - def header_past_months - out = [] - report.step_past_months { |date| out << l(date, format: :month) } - out - end + def header_past_months_summary + ['Total', '⌀'] if report.past_months? + end - def header_past_months_summary - ['Total', '⌀'] if report.past_months? - end + def header_future_months + out = [] + report.step_future_months { |date| out << l(date, format: :month) } + out + end - def header_future_months - out = [] - report.step_future_months { |date| out << l(date, format: :month) } - out - end + def entries + @entries ||= + if report.hours_without_entry? + report.entries + [nil] + else + report.entries + end + end + + # Rows - def entries - @entries ||= - if report.hours_without_entry? - report.entries + [nil] - else - report.entries + def row + [row_grouping, row_past_months, row_past_months_summary, row_future_months].flatten end - end - # Rows + def row_grouping + entry.presence || "#{report.grouping_name_human} nicht zugewiesen" + end - def row - [row_grouping, row_past_months, row_past_months_summary, row_future_months].flatten - end + def row_past_months + out = [] + report.step_past_months do |date| + out << format_number(report.ordertime_hours[[entry.try(:id), date]] || 0, 0) + end + out + end - def row_grouping - entry.presence || "#{report.grouping_name_human} nicht zugewiesen" - end + def row_past_months_summary + return unless report.past_months? - def row_past_months - out = [] - report.step_past_months do |date| - out << format_number(report.ordertime_hours[[entry.try(:id), date]] || 0, 0) - end - out - end + [ + format_number(report.total_ordertime_hours_per_entry(entry), 0), + format_number(report.average_ordertime_hours_per_entry(entry), 0) + ] + end - def row_past_months_summary - if report.past_months? - [ - format_number(report.total_ordertime_hours_per_entry(entry), 0), - format_number(report.average_ordertime_hours_per_entry(entry), 0) - ] - end - end + def row_future_months + out = [] + report.step_future_months do |date| + out << format_number(report.planning_hours[[entry.try(:id), date]] || 0, 0) + end + out + end - def row_future_months - out = [] - report.step_future_months do |date| - out << format_number(report.planning_hours[[entry.try(:id), date]] || 0, 0) - end - out - end + # Footer - # Footer + def footer + [footer_total, footer_past_months, footer_past_months_summary, footer_future_months].flatten + end - def footer - [footer_total, footer_past_months, footer_past_months_summary, footer_future_months].flatten - end + def footer_total + 'Total' + end - def footer_total - 'Total' - end + def footer_past_months + out = [] + report.step_past_months do |date| + out << format_number(report.total_ordertime_hours_per_month[date] || 0, 0) + end + out + end - def footer_past_months - out = [] - report.step_past_months do |date| - out << format_number(report.total_ordertime_hours_per_month[date] || 0, 0) - end - out - end + def footer_past_months_summary + return unless report.past_months? - def footer_past_months_summary - if report.past_months? - [ - format_number(report.total_ordertime_hours_overall, 0), - format_number(report.average_ordertime_hours_overall, 0) - ] - end - end + [ + format_number(report.total_ordertime_hours_overall, 0), + format_number(report.average_ordertime_hours_overall, 0) + ] + end - def footer_future_months - out = [] - report.step_future_months do |date| - out << format_number(report.total_planning_hours_per_month[date] || 0, 0) - end - out - end + def footer_future_months + out = [] + report.step_future_months do |date| + out << format_number(report.total_planning_hours_per_month[date] || 0, 0) + end + out + end - def l(*args) - I18n.l(*args) - end + def l(...) + I18n.l(...) + end - def format_number(number, precision = nil) - ActionController::Base.helpers.number_with_precision(number, precision: precision, delimiter: nil, separator: '.') + def format_number(number, precision = nil) + ActionController::Base.helpers.number_with_precision(number, precision:, delimiter: nil, separator: '.') + end + end end end diff --git a/app/domain/reports/revenue/department.rb b/app/domain/reports/revenue/department.rb index 61d70bf13..9f571c7fc 100644 --- a/app/domain/reports/revenue/department.rb +++ b/app/domain/reports/revenue/department.rb @@ -1,11 +1,15 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -module Reports::Revenue - class Department < Base - self.grouping_model = ::Department - self.grouping_fk = :department_id +module Reports + module Revenue + class Department < Base + self.grouping_model = ::Department + self.grouping_fk = :department_id + end end end diff --git a/app/domain/reports/revenue/portfolio_item.rb b/app/domain/reports/revenue/portfolio_item.rb index 0bdb49ea7..a5a6c74f8 100644 --- a/app/domain/reports/revenue/portfolio_item.rb +++ b/app/domain/reports/revenue/portfolio_item.rb @@ -1,11 +1,15 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -module Reports::Revenue - class PortfolioItem < Base - self.grouping_model = ::PortfolioItem - self.grouping_fk = :portfolio_item_id +module Reports + module Revenue + class PortfolioItem < Base + self.grouping_model = ::PortfolioItem + self.grouping_fk = :portfolio_item_id + end end end diff --git a/app/domain/reports/revenue/sector.rb b/app/domain/reports/revenue/sector.rb index b518dffd3..fac5e86cb 100644 --- a/app/domain/reports/revenue/sector.rb +++ b/app/domain/reports/revenue/sector.rb @@ -1,23 +1,27 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -module Reports::Revenue - class Sector < Base - self.grouping_model = ::Sector - self.grouping_fk = :sector_id +module Reports + module Revenue + class Sector < Base + self.grouping_model = ::Sector + self.grouping_fk = :sector_id - def load_ordertimes(period = past_period) - super - .joins('LEFT JOIN clients ON clients.work_item_id = ANY (work_items.path_ids)') - .joins('LEFT JOIN sectors ON sectors.id = clients.sector_id') - end + def load_ordertimes(period = past_period) + super + .joins('LEFT JOIN clients ON clients.work_item_id = ANY (work_items.path_ids)') + .joins('LEFT JOIN sectors ON sectors.id = clients.sector_id') + end - def load_plannings(period) - super - .joins('LEFT JOIN clients ON clients.work_item_id = ANY (work_items.path_ids)') - .joins('LEFT JOIN sectors ON sectors.id = clients.sector_id') + def load_plannings(period) + super + .joins('LEFT JOIN clients ON clients.work_item_id = ANY (work_items.path_ids)') + .joins('LEFT JOIN sectors ON sectors.id = clients.sector_id') + end end end end diff --git a/app/domain/reports/revenue/service.rb b/app/domain/reports/revenue/service.rb index b537d7b9f..048c952b8 100644 --- a/app/domain/reports/revenue/service.rb +++ b/app/domain/reports/revenue/service.rb @@ -1,11 +1,15 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -module Reports::Revenue - class Service < Base - self.grouping_model = ::Service - self.grouping_fk = :service_id +module Reports + module Revenue + class Service < Base + self.grouping_model = ::Service + self.grouping_fk = :service_id + end end end diff --git a/app/domain/reports/role_distribution_report.rb b/app/domain/reports/role_distribution_report.rb index a7985e0a4..e55afa76f 100644 --- a/app/domain/reports/role_distribution_report.rb +++ b/app/domain/reports/role_distribution_report.rb @@ -1,170 +1,174 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class RoleDistributionReport - def initialize(date) - @date = date - @filename_prefix = 'puzzletime_funktionsanteile' - end - - def filename - "#{@filename_prefix}_#{format_date_short(@date)}.csv" - end +module Reports + class RoleDistributionReport + def initialize(date) + @date = date + @filename_prefix = 'puzzletime_funktionsanteile' + end - def to_csv - CSV.generate do |csv| - add_header(csv) - add_employees(csv) + def filename + "#{@filename_prefix}_#{format_date_short(@date)}.csv" end - end - private + def to_csv + CSV.generate do |csv| + add_header(csv) + add_employees(csv) + end + end - def period - Period.day_for(@date) - end + private - def add_header(csv) - header = [ - Employee.model_name.human, - 'Anstellung', - 'Wertschöpfung' - ] + categories.map(&:second) - csv << ["Funktionsanteile per #{format_date_long(@date)}, #{format_business_year(@date)}"] + - Array.new(header.length - 1, '') - csv << header - end + def period + Period.day_for(@date) + end - def add_employees(csv) - @totals = {} - groups = employees.map { |e| [e.department_id, e.department_name] }.uniq - groups.each do |department_id, department_name| - add_department(csv, department_name) - employees.select { |e| e.department_id == department_id } - .each { |e| add_employee(csv, e) } - add_department_totals(csv, department_id, department_name) + def add_header(csv) + header = [ + Employee.model_name.human, + 'Anstellung', + 'Wertschöpfung' + ] + categories.map(&:second) + csv << (["Funktionsanteile per #{format_date_long(@date)}, #{format_business_year(@date)}"] + + Array.new(header.length - 1, '')) + csv << header end - add_overall_totals(csv) - end - def add_department(csv, name) - add_empty(csv) - csv << ["#{Department.model_name.human} #{name}"] + Array.new(categories.length + 2, '') - end + def add_employees(csv) + @totals = {} + groups = employees.map { |e| [e.department_id, e.department_name] }.uniq + groups.each do |department_id, department_name| + add_department(csv, department_name) + employees.select { |e| e.department_id == department_id } + .each { |e| add_employee(csv, e) } + add_department_totals(csv, department_id, department_name) + end + add_overall_totals(csv) + end - def add_employee(csv, employee) - csv << [ - employee.to_s, - format_percent(employee.current_percent_value), - format_percent(employee.added_value_percent) - ] + categories.map { |category_id, _category_name| format_percent(category_percent_for(employee, category_id)) } - sum_up_employee(employee) - end + def add_department(csv, name) + add_empty(csv) + csv << (["#{Department.model_name.human} #{name}"] + Array.new(categories.length + 2, '')) + end - def add_department_totals(csv, id, name) - totals = @totals[id] - add_empty(csv) - csv << ["Total #{name}", format_percent(totals[:current_percent_value]), - format_percent(totals[:added_value_percent])] + - categories.map { |category_id, _category_name| format_percent(totals["category_#{category_id}".to_sym]) } - end + def add_employee(csv, employee) + csv << ([ + employee.to_s, + format_percent(employee.current_percent_value), + format_percent(employee.added_value_percent) + ] + categories.map { |category_id, _category_name| format_percent(category_percent_for(employee, category_id)) }) + sum_up_employee(employee) + end - def add_overall_totals(csv) - add_empty(csv) - csv << ['', 'Anstellung', 'Wertschöpfung'] + - categories.map { |_category_id, category_name| category_name } - csv << ['Total FTE', format_fte(overall_total(:current_percent_value)), - format_fte(overall_total(:added_value_percent))] + - categories.map { |category_id, _category_name| format_fte(overall_total("category_#{category_id}".to_sym)) } - end + def add_department_totals(csv, id, name) + totals = @totals[id] + add_empty(csv) + csv << (["Total #{name}", format_percent(totals[:current_percent_value]), + format_percent(totals[:added_value_percent])] + + categories.map { |category_id, _category_name| format_percent(totals[:"category_#{category_id}"]) }) + end - def add_empty(csv) - csv << Array.new(categories.length + 3, '') - end + def add_overall_totals(csv) + add_empty(csv) + csv << (['', 'Anstellung', 'Wertschöpfung'] + + categories.map { |_category_id, category_name| category_name }) + csv << (['Total FTE', format_fte(overall_total(:current_percent_value)), + format_fte(overall_total(:added_value_percent))] + + categories.map { |category_id, _category_name| format_fte(overall_total(:"category_#{category_id}")) }) + end - def category_percent_for(employee, category_id) - (category_percents[employee.id] || {})[category_id].to_f - end + def add_empty(csv) + csv << Array.new(categories.length + 3, '') + end - def sum_up_employee(employee) - totals = @totals[employee.department_id] ||= {} - [:current_percent_value, :added_value_percent].each do |attr| - totals[attr] = totals[attr].to_f + employee.send(attr).to_f + def category_percent_for(employee, category_id) + (category_percents[employee.id] || {})[category_id].to_f end - categories.each do |category_id, _category_name| - attr = "category_#{category_id}".to_sym - totals[attr] = totals[attr].to_f + category_percent_for(employee, category_id) + + def sum_up_employee(employee) + totals = @totals[employee.department_id] ||= {} + %i[current_percent_value added_value_percent].each do |attr| + totals[attr] = totals[attr].to_f + employee.send(attr).to_f + end + categories.each do |(category_id, _)| + attr = :"category_#{category_id}" + totals[attr] = totals[attr].to_f + category_percent_for(employee, category_id) + end end - end - def overall_total(attr) - @totals.sum { |_k, v| v[attr] } - end + def overall_total(attr) + @totals.sum { |_k, v| v[attr] } + end - def employees - @employees ||= Employee.select('employees.id, employees.lastname, employees.firstname, ' \ - 'department_id, departments.name AS department_name, ' \ - 'em.percent AS current_percent_value, ' \ - 'brp.percent AS unbillable_percent, ' \ - '(em.percent - COALESCE(brp.percent, 0)) AS added_value_percent') - .employed_ones(period) - .joins(:department) - .joins("LEFT JOIN (#{unbillable_roles_percent.to_sql}) AS brp ON employees.id = brp.id") - .reorder('department_name, lastname, firstname') - end + def employees + @employees ||= Employee.select('employees.id, employees.lastname, employees.firstname, ' \ + 'department_id, departments.name AS department_name, ' \ + 'em.percent AS current_percent_value, ' \ + 'brp.percent AS unbillable_percent, ' \ + '(em.percent - COALESCE(brp.percent, 0)) AS added_value_percent') + .employed_ones(period) + .joins(:department) + .joins("LEFT JOIN (#{unbillable_roles_percent.to_sql}) AS brp ON employees.id = brp.id") + .reorder('department_name, lastname, firstname') + end - def unbillable_roles_percent - Employee.select('employees.id, SUM(eres.percent) AS percent') - .employed_ones(period, false) - .joins('LEFT JOIN employment_roles_employments eres ON (em.id = eres.employment_id)') - .joins('INNER JOIN employment_roles ers ON (eres.employment_role_id = ers.id AND NOT ers.billable)') - .group('employees.id') - end + def unbillable_roles_percent + Employee.select('employees.id, SUM(eres.percent) AS percent') + .employed_ones(period, false) + .joins('LEFT JOIN employment_roles_employments eres ON (em.id = eres.employment_id)') + .joins('INNER JOIN employment_roles ers ON (eres.employment_role_id = ers.id AND NOT ers.billable)') + .group('employees.id') + end - def categories - @categories ||= - Employee.employed_ones(period) - .joins('INNER JOIN employment_roles_employments ere ON ere.employment_id = em.id') - .joins('INNER JOIN employment_roles er ON er.id = ere.employment_role_id') - .joins('INNER JOIN employment_role_categories erc ON erc.id = er.employment_role_category_id') - .reorder('erc.name') - .pluck('erc.id, erc.name') - end + def categories + @categories ||= + Employee.employed_ones(period) + .joins('INNER JOIN employment_roles_employments ere ON ere.employment_id = em.id') + .joins('INNER JOIN employment_roles er ON er.id = ere.employment_role_id') + .joins('INNER JOIN employment_role_categories erc ON erc.id = er.employment_role_category_id') + .reorder('erc.name') + .pluck('erc.id, erc.name') + end - def category_percents - @category_percents ||= - Employee.employed_ones(period, false) - .joins('INNER JOIN employment_roles_employments ere ON ere.employment_id = em.id') - .joins('INNER JOIN employment_roles er ON er.id = ere.employment_role_id') - .joins('INNER JOIN employment_role_categories erc ON erc.id = er.employment_role_category_id') - .group('employees.id', 'erc.id') - .pluck('employees.id, erc.id, SUM(COALESCE(ere.percent, 0)) AS percent') - .each_with_object({}) do |(employee_id, category_id, percent), o| - o[employee_id] ||= {} - o[employee_id][category_id] = percent - end - end + def category_percents + @category_percents ||= + Employee.employed_ones(period, false) + .joins('INNER JOIN employment_roles_employments ere ON ere.employment_id = em.id') + .joins('INNER JOIN employment_roles er ON er.id = ere.employment_role_id') + .joins('INNER JOIN employment_role_categories erc ON erc.id = er.employment_role_category_id') + .group('employees.id', 'erc.id') + .pluck(Arel.sql('employees.id, erc.id, SUM(COALESCE(ere.percent, 0)) AS percent')) + .each_with_object({}) do |(employee_id, category_id, percent), o| + o[employee_id] ||= {} + o[employee_id][category_id] = percent + end + end - def format_date_short(date) - I18n.l(date, format: '%Y%m%d') - end + def format_date_short(date) + I18n.l(date, format: '%Y%m%d') + end - def format_date_long(date) - I18n.l(date, format: '%d.%m.%Y') - end + def format_date_long(date) + I18n.l(date, format: '%d.%m.%Y') + end - def format_business_year(date) - period = Period.business_year_for(date) - "GJ #{[period.start_date.year, period.end_date.year].uniq.join('/')}" - end + def format_business_year(date) + period = Period.business_year_for(date) + "GJ #{[period.start_date.year, period.end_date.year].uniq.join('/')}" + end - def format_percent(value) - value.to_f.round(2).to_s + '%' - end + def format_percent(value) + "#{value.to_f.round(2)}%" + end - def format_fte(value) - (value.to_f / 100).round(2) + def format_fte(value) + (value.to_f / 100).round(2) + end end end diff --git a/app/domain/reports/workload.rb b/app/domain/reports/workload.rb index e8fd337c2..afd6ac662 100644 --- a/app/domain/reports/workload.rb +++ b/app/domain/reports/workload.rb @@ -1,149 +1,151 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Reports::Workload - include Filterable - - attr_reader :period, :params, :department - - WORKTIME_FIELDS = [ - :type, - :employee_id, - :department_id, - :hours, - :work_item_id, - :path_ids, - :billable, - :absence_id, - :payed - ].freeze - - def initialize(period, department, params = {}) - @period = period - @department = department - @params = params - end +module Reports + class Workload + include Filterable + + attr_reader :period, :params, :department + + WORKTIME_FIELDS = %i[ + type + employee_id + department_id + hours + work_item_id + path_ids + billable + absence_id + payed + ].freeze + + def initialize(period, department, params = {}) + @period = period + @department = department + @params = params + end - def filters_defined? - period.limited? && department.present? - end + def filters_defined? + period&.limited? && department.present? + end - def present? - department_worktimes.present? - end + delegate :present?, to: :department_worktimes - def all_employees - @all_employees ||= Employee.all.to_a - end + def all_employees + @all_employees ||= Employee.all.to_a + end - def department_period_employments - @department_period_employments ||= begin - department_employee_ids = all_employees.select { |e| e.department_id == department.id }.map(&:id) - period_employments.select { |e| department_employee_ids.include?(e.employee_id) } + def department_period_employments + @department_period_employments ||= begin + department_employee_ids = all_employees.select { |e| e.department_id == department.id }.map(&:id) + period_employments.select { |e| department_employee_ids.include?(e.employee_id) } + end end - end - def department_period_employees_with_employment_or_worktime - employee_ids = (department_period_employments + department_worktimes).map(&:employee_id).uniq - all_employees.select { |employee| employee_ids.include?(employee.id) } - end + def department_period_employees_with_employment_or_worktime + employee_ids = (department_period_employments + department_worktimes).map(&:employee_id).uniq + all_employees.select { |employee| employee_ids.include?(employee.id) } + end - def summary - [ - Reports::Workload::SummaryEntry.new(Company.name, period, period_employments, worktimes), - Reports::Workload::SummaryEntry.new(department, period, department_period_employments, department_worktimes) - ] - end + def summary + [ + Reports::Workload::SummaryEntry.new(Company.name, period, period_employments, worktimes), + Reports::Workload::SummaryEntry.new(department, period, department_period_employments, department_worktimes) + ] + end - def entries - @entries ||= sort_entries(build_entries) - end + def entries + @entries ||= sort_entries(build_entries) + end - def department_worktimes - @department_worktimes ||= worktimes.select do |worktime_entry| - worktime_entry.department_id == department.id + def department_worktimes + @department_worktimes ||= worktimes.select do |worktime_entry| + worktime_entry.department_id == department.id + end end - end - private + private - def worktimes - @worktimes ||= build_worktimes - end + def worktimes + @worktimes ||= build_worktimes + end - def build_worktimes - order_work_item_ids = Order.all.pluck(:work_item_id) - order_work_items = WorkItem.where(id: order_work_item_ids).to_a - worktimes_query.pluck(*WORKTIME_FIELDS).map do |row| - WorktimeEntry.new(*row).tap do |worktime_entry| - if worktime_entry.ordertime? - worktime_entry.order_work_item = order_work_items.detect do |work_item| - worktime_entry.path_ids.include?(work_item.id) + def build_worktimes + order_work_item_ids = Order.pluck(:work_item_id) + order_work_items = WorkItem.where(id: order_work_item_ids).to_a + worktimes_query.pluck(*WORKTIME_FIELDS).map do |row| + WorktimeEntry.new(*row).tap do |worktime_entry| + if worktime_entry.ordertime? + worktime_entry.order_work_item = order_work_items.detect do |work_item| + worktime_entry.path_ids.include?(work_item.id) + end end end end end - end - def worktimes_query - Worktime. - in_period(period). - joins('LEFT OUTER JOIN work_items ON work_items.id = worktimes.work_item_id'). - joins('LEFT OUTER JOIN absences ON absences.id = worktimes.absence_id'). - joins(:employee) - end + def worktimes_query + Worktime + .in_period(period) + .joins('LEFT OUTER JOIN work_items ON work_items.id = worktimes.work_item_id') + .joins('LEFT OUTER JOIN absences ON absences.id = worktimes.absence_id') + .joins(:employee) + end - def build_entries - employee_employments_map = period_employments.group_by(&:employee_id) - employee_department_worktimes_map = department_worktimes.group_by(&:employee_id) - department_period_employees_with_employment_or_worktime.map do |employee| - employments = employee_employments_map[employee.id] || [] - worktimes = employee_department_worktimes_map[employee.id] || [] - Reports::Workload::EmployeeEntry.new(employee, period, employments, worktimes) + def build_entries + employee_employments_map = period_employments.group_by(&:employee_id) + employee_department_worktimes_map = department_worktimes.group_by(&:employee_id) + department_period_employees_with_employment_or_worktime.map do |employee| + employments = employee_employments_map[employee.id] || [] + worktimes = employee_department_worktimes_map[employee.id] || [] + Reports::Workload::EmployeeEntry.new(employee, period, employments, worktimes) + end end - end - def period_employments - @period_employments ||= load_employments - end + def period_employments + @period_employments ||= load_employments + end - def load_employments - Employment. - where('(end_date IS NULL OR end_date >= ?) AND start_date <= ?', - period.start_date, period.end_date). - reorder('start_date').to_a - end + def load_employments + Employment + .where('(end_date IS NULL OR end_date >= ?) AND start_date <= ?', + period.start_date, period.end_date) + .reorder('start_date').to_a + end - def sort_entries(entries) - dir = params[:sort_dir].to_s.casecmp('desc').zero? ? 1 : -1 - if sort_by_employee? - sort_by_employee(entries, dir) - elsif sort_by_number? - sort_by_number(entries, dir) - else - entries + def sort_entries(entries) + dir = params[:sort_dir].to_s.casecmp('desc').zero? ? 1 : -1 + if sort_by_employee? + sort_by_employee(entries, dir) + elsif sort_by_number? + sort_by_number(entries, dir) + else + entries + end end - end - def sort_by_employee? - params[:sort] == 'employee' || params[:sort].blank? - end + def sort_by_employee? + params[:sort] == 'employee' || params[:sort].blank? + end - def sort_by_number? - %w(must_hours worktime_balance ordertime_hours absencetime_hours workload billability).include?(params[:sort]) - end + def sort_by_number? + %w[must_hours worktime_balance ordertime_hours absencetime_hours workload billability].include?(params[:sort]) + end - def sort_by_employee(entries, dir) - entries.sort_by(&:to_s).tap do |sorted_entries| - sorted_entries.reverse! if dir > 0 + def sort_by_employee(entries, dir) + entries.sort_by(&:to_s).tap do |sorted_entries| + sorted_entries.reverse! if dir.positive? + end end - end - def sort_by_number(entries, dir) - entries.sort_by do |e| - e.send(params[:sort]).to_f * dir + def sort_by_number(entries, dir) + entries.sort_by do |e| + e.send(params[:sort]).to_f * dir + end end end end diff --git a/app/domain/reports/workload/base_entry.rb b/app/domain/reports/workload/base_entry.rb index 6049b7112..67d8afb4e 100644 --- a/app/domain/reports/workload/base_entry.rb +++ b/app/domain/reports/workload/base_entry.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -43,11 +45,11 @@ def billable_hours end def workload - ordertime_hours > 0 ? 100 * external_client_hours / ordertime_hours : 0 + ordertime_hours.positive? ? 100 * external_client_hours / ordertime_hours : 0 end def billability - external_client_hours > 0 ? 100 * billable_hours / external_client_hours : 0 + external_client_hours.positive? ? 100 * billable_hours / external_client_hours : 0 end private diff --git a/app/domain/reports/workload/employee_entry.rb b/app/domain/reports/workload/employee_entry.rb index 50938b45e..8426e797d 100644 --- a/app/domain/reports/workload/employee_entry.rb +++ b/app/domain/reports/workload/employee_entry.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -25,7 +27,7 @@ def build_entries ordertimes.group_by(&:order_work_item).map do |work_item, ordertimes| ordertime_hours = ordertimes.sum(&:hours) billable_hours = ordertimes.select(&:billable).sum(&:hours) || 0 - billability = billable_hours > 0 ? billable_hours / ordertime_hours : 0 + billability = billable_hours.positive? ? billable_hours / ordertime_hours : 0 Reports::Workload::OrdertimeEntry.new(work_item, ordertime_hours, billability) end end diff --git a/app/domain/reports/workload/ordertime_entry.rb b/app/domain/reports/workload/ordertime_entry.rb index 62a78799a..15f7f9aa7 100644 --- a/app/domain/reports/workload/ordertime_entry.rb +++ b/app/domain/reports/workload/ordertime_entry.rb @@ -1,22 +1,26 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Reports::Workload::OrdertimeEntry < Struct.new(:work_item, :hours, :billability) - def id - work_item.id - end +module Reports + class Workload + OrdertimeEntry = Struct.new(:work_item, :hours, :billability) do + delegate :id, to: :work_item - def label - work_item.path_shortnames - end + def label + work_item.path_shortnames + end - def description - work_item.name - end + def description + work_item.name + end - def billability_percent - 100 * billability + def billability_percent + 100 * billability + end + end end end diff --git a/app/domain/reports/workload/summary_entry.rb b/app/domain/reports/workload/summary_entry.rb index ebc26f5d8..d770839cb 100644 --- a/app/domain/reports/workload/summary_entry.rb +++ b/app/domain/reports/workload/summary_entry.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -5,7 +7,7 @@ module Reports class Workload - class Reports::Workload::SummaryEntry < BaseEntry + class SummaryEntry < BaseEntry attr_reader :label def initialize(label, period, employments, worktimes) @@ -22,7 +24,7 @@ def employment_fte end def absolute_billability - ordertime_hours > 0 ? 100 * billable_hours / ordertime_hours : 0 + ordertime_hours.positive? ? 100 * billable_hours / ordertime_hours : 0 end private diff --git a/app/domain/reports/workload/worktime_entry.rb b/app/domain/reports/workload/worktime_entry.rb index 3f900f010..034cc1718 100644 --- a/app/domain/reports/workload/worktime_entry.rb +++ b/app/domain/reports/workload/worktime_entry.rb @@ -1,18 +1,24 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -class Reports::Workload::WorktimeEntry < Struct.new(*Reports::Workload::WORKTIME_FIELDS, :order_work_item) - def absencetime? - type == Absencetime.name - end +module Reports + class Workload + WorktimeEntry = Struct.new(*Reports::Workload::WORKTIME_FIELDS, :order_work_item) do + def absencetime? + type == Absencetime.name + end - def ordertime? - type == Ordertime.name - end + def ordertime? + type == Ordertime.name + end - def external_client? - Array.wrap(path_ids).exclude?(Company.work_item_id) + def external_client? + Array.wrap(path_ids).exclude?(Company.work_item_id) + end + end end end diff --git a/app/helpers/accounting_posts_helper.rb b/app/helpers/accounting_posts_helper.rb index 5405fad68..09548c3f4 100644 --- a/app/helpers/accounting_posts_helper.rb +++ b/app/helpers/accounting_posts_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module AccountingPostsHelper def blocking_worktimes(type) worktimes = @accounting_post.worktimes.includes(:employee) @@ -30,6 +32,6 @@ def blocking_list(worktimes) worktime_counts(worktimes).collect do |k, v| employee = Employee.find(k) "#{employee.firstname} #{employee.lastname}: #{v} Buchungen" - end.join(", ") + end.join(', ') end end diff --git a/app/helpers/actions_helper.rb b/app/helpers/actions_helper.rb index c5af756cd..b8c9abcf2 100644 --- a/app/helpers/actions_helper.rb +++ b/app/helpers/actions_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -37,7 +39,7 @@ def edit_action_link(path = nil) return unless can?(:edit, entry) path ||= path_args(entry) - path = path.is_a?(String) ? path : edit_polymorphic_path(path) + path = edit_polymorphic_path(path) unless path.is_a?(String) action_link(action_icon('edit', ti('link.edit')), path) end @@ -56,7 +58,7 @@ def index_action_link(path = nil, url_options = { returning: true }) return unless can?(:index, model_class) path ||= path_args(model_class) - path = path.is_a?(String) ? path : polymorphic_path(path, url_options) + path = polymorphic_path(path, url_options) unless path.is_a?(String) action_link(ti('link.list', model: models_label(true)), path) end @@ -66,7 +68,7 @@ def add_action_link(path = nil, url_options = {}) return unless can?(:new, model_class) path ||= path_args(model_class) - path = path.is_a?(String) ? path : new_polymorphic_path(path, url_options) + path = new_polymorphic_path(path, url_options) unless path.is_a?(String) action_link(action_icon('add', ti('link.add')), path) end diff --git a/app/helpers/attachment_helper.rb b/app/helpers/attachment_helper.rb index fc765b165..7ffef1b55 100644 --- a/app/helpers/attachment_helper.rb +++ b/app/helpers/attachment_helper.rb @@ -14,15 +14,13 @@ def attachment_displayable?(receipt) end def attachment_image_tag(obj, **options) - transformations = - { - auto_orient: true, - resize: '800x1200>' - } + transformations = { + resize_to_limit: [800, 1200] + } image = attachment_image(obj, transformations) tag = image_tag(image, options) - return attachment_show_link(obj, tag, options) if options[:show_link] + return attachment_show_link(obj, tag, **options) if options[:show_link] tag end diff --git a/app/helpers/clients_helper.rb b/app/helpers/clients_helper.rb index 5c3792d71..7521f7bb4 100644 --- a/app/helpers/clients_helper.rb +++ b/app/helpers/clients_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/helpers/completable_helper.rb b/app/helpers/completable_helper.rb index 8075df62d..2d282aefa 100644 --- a/app/helpers/completable_helper.rb +++ b/app/helpers/completable_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -13,12 +15,13 @@ def completed_icon(date) end def recently_completed(date) + # logic should match Employee::pending_worktimes_commit date && date >= Time.zone.today.end_of_month - 1.month end def format_month(date) - if date - I18n.l(date, format: :month) - end + return unless date + + I18n.l(date, format: :month) end end diff --git a/app/helpers/contacts_helper.rb b/app/helpers/contacts_helper.rb index 01e0e2d5c..830ada752 100644 --- a/app/helpers/contacts_helper.rb +++ b/app/helpers/contacts_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/helpers/contracts_helper.rb b/app/helpers/contracts_helper.rb index 93eba7bb8..d7a2f76d2 100644 --- a/app/helpers/contracts_helper.rb +++ b/app/helpers/contracts_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/helpers/crm_helper.rb b/app/helpers/crm_helper.rb index 8586db4be..48a4be444 100644 --- a/app/helpers/crm_helper.rb +++ b/app/helpers/crm_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/helpers/custom_lists_helper.rb b/app/helpers/custom_lists_helper.rb index 09f2eea73..40112814a 100644 --- a/app/helpers/custom_lists_helper.rb +++ b/app/helpers/custom_lists_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/helpers/dry_crud/form/builder.rb b/app/helpers/dry_crud/form/builder.rb index 5d4784953..8f613c6ed 100644 --- a/app/helpers/dry_crud/form/builder.rb +++ b/app/helpers/dry_crud/form/builder.rb @@ -1,379 +1,380 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -module DryCrud::Form - # A form builder that automatically selects the corresponding input field - # for ActiveRecord column types. Convenience methods for each column type - # allow one to customize the different fields. - # - # All field methods may be prefixed with +labeled_+ in order to render - # a standard label, required mark and an optional help block with them. - # - # Use #labeled_input_field or #input_field to render a input field - # corresponding to the given attribute. - # - # See the Control class for how to customize the html rendered for a - # single input field. - class Builder < ActionView::Helpers::FormBuilder - class_attribute :control_class - self.control_class = Control - - attr_reader :template - - delegate :association, :column_type, :column_property, :captionize, - :ti, :ta, :link_to, :content_tag, :safe_join, :capture, - :add_css_class, :assoc_and_id_attr, :icon, - to: :template - - ### INPUT FIELDS - - # Render multiple input controls together with a label for the given - # attributes. - def labeled_input_fields(*attrs) - options = attrs.extract_options! - safe_join(attrs) { |a| labeled_input_field(a, options.dup) } - end - - # Render a corresponding input control and label for the given attribute. - # The input field is chosen based on the ActiveRecord column type. +module DryCrud + module Form + # A form builder that automatically selects the corresponding input field + # for ActiveRecord column types. Convenience methods for each column type + # allow one to customize the different fields. # - # The following options may be passed: - # * :addon - Addon content displayd just after the input field. - # * :help - A help text displayd below the input field. - # * :span - Number of columns the input field should span. - # * :caption - Different caption for the label. - # * :field_method - Different method to create the input field. - # - # Use additional html_options for the input element. - def labeled_input_field(attr, html_options = {}) - control_class.new(self, attr, html_options).render_labeled - end - - # Render a corresponding input control for the given attribute. - # The input field is chosen based on the ActiveRecord column type. + # All field methods may be prefixed with +labeled_+ in order to render + # a standard label, required mark and an optional help block with them. # - # The following options may be passed: - # * :addon - Addon content displayd just after the input field. - # * :help - A help text displayd below the input field. - # * :span - Number of columns the input field should span. - # * :field_method - Different method to create the input field. + # Use #labeled_input_field or #input_field to render a input field + # corresponding to the given attribute. # - # Use additional html_options for the input element. - def input_field(attr, html_options = {}) - control_class.new(self, attr, html_options).render_content - end - - # Render a standard string field with column contraints. - def string_field(attr, html_options = {}) - html_options[:maxlength] ||= column_property(@object, attr, :limit) - html_options[:size] ||= 30 - text_field(attr, html_options) - end + # See the Control class for how to customize the html rendered for a + # single input field. + class Builder < ActionView::Helpers::FormBuilder + class_attribute :control_class + self.control_class = Control + + attr_reader :template + + delegate :association, :column_type, :column_property, :captionize, + :ti, :ta, :link_to, :content_tag, :safe_join, :capture, + :add_css_class, :assoc_and_id_attr, :icon, + to: :template + + ### INPUT FIELDS + + # Render multiple input controls together with a label for the given + # attributes. + def labeled_input_fields(*attrs) + options = attrs.extract_options! + safe_join(attrs) { |a| labeled_input_field(a, options.dup) } + end - def text_field(attr, html_options = {}) - add_css_class(html_options, 'form-control') - super(attr, html_options) - end + # Render a corresponding input control and label for the given attribute. + # The input field is chosen based on the ActiveRecord column type. + # + # The following options may be passed: + # * :addon - Addon content displayd just after the input field. + # * :help - A help text displayd below the input field. + # * :span - Number of columns the input field should span. + # * :caption - Different caption for the label. + # * :field_method - Different method to create the input field. + # + # Use additional html_options for the input element. + def labeled_input_field(attr, html_options = {}) + control_class.new(self, attr, html_options).render_labeled + end - def email_field(attr, html_options = {}) - add_css_class(html_options, 'form-control') - super(attr, html_options) - end + # Render a corresponding input control for the given attribute. + # The input field is chosen based on the ActiveRecord column type. + # + # The following options may be passed: + # * :addon - Addon content displayd just after the input field. + # * :help - A help text displayd below the input field. + # * :span - Number of columns the input field should span. + # * :field_method - Different method to create the input field. + # + # Use additional html_options for the input element. + def input_field(attr, html_options = {}) + control_class.new(self, attr, html_options).render_content + end - # Render a boolean field. - def boolean_field(attr, html_options = {}) - content_tag(:div, class: 'checkbox') do - content_tag(:label) do - detail = html_options.delete(:detail) || ' '.html_safe - safe_join([check_box(attr, html_options), ' ', detail]) - end + # Render a standard string field with column contraints. + def string_field(attr, html_options = {}) + html_options[:maxlength] ||= column_property(@object, attr, :limit) + html_options[:size] ||= 30 + text_field(attr, html_options) end - end - # Customize the standard text area to have 5 rows by default. - def text_area(attr, html_options = {}) - html_options[:rows] ||= 5 - html_options[:cols] ||= 50 - add_css_class(html_options, 'form-control') - super - end + def text_field(attr, html_options = {}) + add_css_class(html_options, 'form-control') + super(attr, html_options) + end - # Render a field to select a date. - def date_field(attr, html_options = {}) - format = html_options[:data] && html_options[:data][:format] - date = date_value(attr, format) - html_options[:size] = 15 - html_options[:value] = date - add_css_class(html_options, 'date') - with_addon(text_field(attr, html_options), icon(:calendar)) - end + def email_field(attr, html_options = {}) + add_css_class(html_options, 'form-control') + super(attr, html_options) + end - def date_value(attr, format) - raw = nil - if @object.respond_to?(:_timeliness_raw_value_for) - raw = @object._timeliness_raw_value_for(attr.to_s) + # Render a boolean field. + def boolean_field(attr, html_options = {}) + content_tag(:div, class: 'checkbox') do + content_tag(:label) do + detail = html_options.delete(:detail) || ' '.html_safe + safe_join([check_box(attr, html_options), ' ', detail]) + end + end end - if raw.is_a?(String) - raw - else - val = @object.send(attr) - val.is_a?(Date) ? template.l(val, format: format) : val + + # Customize the standard text area to have 5 rows by default. + def text_area(attr, html_options = {}) + html_options[:rows] ||= 5 + html_options[:cols] ||= 50 + add_css_class(html_options, 'form-control') + super end - end - def number_field(attr, html_options = {}) - html_options[:size] ||= 15 - html_options[:type] ||= 'number' - html_options[:step] ||= 'any' - html_options[:lang] ||= 'de-CH' - text_field(attr, html_options) - end + # Render a field to select a date. + def date_field(attr, html_options = {}) + format = html_options[:data] && html_options[:data][:format] + date = date_value(attr, format) + html_options[:size] = 15 + html_options[:value] = date + add_css_class(html_options, 'date') + with_addon(text_field(attr, html_options), icon(:calendar)) + end - alias integer_field number_field - alias float_field number_field - alias decimal_field number_field + def date_value(attr, format) + raw = nil + raw = @object._timeliness_raw_value_for(attr.to_s) if @object.respond_to?(:_timeliness_raw_value_for) + if raw.is_a?(String) + raw + else + val = @object.send(attr) + val.is_a?(Date) ? template.l(val, format:) : val + end + end - def enum_field(attr, html_options = {}) - add_css_class(html_options, 'form-control') - attr_s = attr.to_s - list = object.class.defined_enums[attr_s].map do |key, _value| - [key, - object.class.human_attribute_name([attr_s.pluralize, key].join('.'))] + def number_field(attr, html_options = {}) + html_options[:size] ||= 15 + html_options[:type] ||= 'number' + html_options[:step] ||= 'any' + html_options[:lang] ||= 'de-CH' + text_field(attr, html_options) end - collection_select(attr, list, :first, :second, - select_options(attr, html_options), - html_options) - end - # Render a select element for a :belongs_to association defined by attr. - # Use additional html_options for the select element. - # To pass a custom element list, specify the list with the :list key or - # define an instance variable with the pluralized name of the association. - def belongs_to_field(attr, html_options = {}) - list = association_entries(attr, html_options).to_a - if list.present? + alias integer_field number_field + alias float_field number_field + alias decimal_field number_field + + def enum_field(attr, html_options = {}) add_css_class(html_options, 'form-control') - collection_select(attr, list, :id, :to_s, + attr_s = attr.to_s + list = object.class.defined_enums[attr_s].map do |key, _value| + [key, + object.class.human_attribute_name([attr_s.pluralize, key].join('.'))] + end + collection_select(attr, list, :first, :second, select_options(attr, html_options), html_options) - else - static_text(ta(:none_available, association(@object, attr)).html_safe) end - end - # Render a multi select element for a :has_many or :has_and_belongs_to_many - # association defined by attr. - # Use additional html_options for the select element. - # To pass a custom element list, specify the list with the :list key or - # define an instance variable with the pluralized name of the association. - def has_many_field(attr, html_options = {}) - html_options[:multiple] = true - add_css_class(html_options, 'multiselect') - belongs_to_field(attr, html_options) - end + # Render a select element for a :belongs_to association defined by attr. + # Use additional html_options for the select element. + # To pass a custom element list, specify the list with the :list key or + # define an instance variable with the pluralized name of the association. + def belongs_to_field(attr, html_options = {}) + list = association_entries(attr, html_options).to_a + if list.present? + add_css_class(html_options, 'form-control') + collection_select(attr, list, :id, :to_s, + select_options(attr, html_options), + html_options) + else + static_text(ta(:none_available, association(@object, attr)).html_safe) + end + end - ### VARIOUS FORM ELEMENTS + # Render a multi select element for a :has_many or :has_and_belongs_to_many + # association defined by attr. + # Use additional html_options for the select element. + # To pass a custom element list, specify the list with the :list key or + # define an instance variable with the pluralized name of the association. + def has_many_field(attr, html_options = {}) + html_options[:multiple] = true + add_css_class(html_options, 'multiselect') + belongs_to_field(attr, html_options) + end - # Render the error messages for the current form. - def error_messages - @template.render('shared/error_messages', - errors: @object.errors, - object: @object) - end + ### VARIOUS FORM ELEMENTS - # Renders the given content with an addon. - def with_addon(content, addon) - content_tag(:div, class: 'input-group') do - content + content_tag(:span, addon, class: 'input-group-addon') + # Render the error messages for the current form. + def error_messages + @template.render('shared/error_messages', + errors: @object.errors, + object: @object) end - end - def static_field(attr, html_options = {}) - static_text(@template.format_attr(@object, attr), html_options) - end + # Renders the given content with an addon. + def with_addon(content, addon) + content_tag(:div, class: 'input-group') do + content + content_tag(:span, addon, class: 'input-group-addon') + end + end - # Renders a static text where otherwise form inputs appear. - def static_text(text, html_options = {}) - add_css_class(html_options, 'form-control-static') - content_tag(:p, text, html_options) - end + def static_field(attr, html_options = {}) + static_text(@template.format_attr(@object, attr), html_options) + end - # Generates a help block for fields - def help_block(text) - content_tag(:p, text, class: 'help-block col-md-5') - end + # Renders a static text where otherwise form inputs appear. + def static_text(text, html_options = {}) + add_css_class(html_options, 'form-control-static') + content_tag(:p, text, html_options) + end + + # Generates a help block for fields + def help_block(text) + content_tag(:p, text, class: 'help-block col-md-5') + end - # Render a submit button and a cancel link for this form. - def standard_actions(submit_label = ti('button.save'), cancel_url = nil) - content_tag(:div, class: 'form-group') do - content_tag(:div, class: 'col-md-offset-2 col-md-8') do - safe_join([submit_button(submit_label), cancel_link(cancel_url)], ' ') + # Render a submit button and a cancel link for this form. + def standard_actions(submit_label = ti('button.save'), cancel_url = nil) + content_tag(:div, class: 'form-group') do + content_tag(:div, class: 'col-md-offset-2 col-md-8') do + safe_join([submit_button(submit_label), cancel_link(cancel_url)], ' ') + end end end - end - # Render only a submit button for this form. - def submit_action(submit_label = ti('button.save')) - content_tag(:div, class: 'form-group') do - content_tag(:div, class: 'col-md-offset-2 col-md-8') do - submit_button(submit_label) + # Render only a submit button for this form. + def submit_action(submit_label = ti('button.save')) + content_tag(:div, class: 'form-group') do + content_tag(:div, class: 'col-md-offset-2 col-md-8') do + submit_button(submit_label) + end end end - end - # Render a standard submit button with the given label. - def submit_button(label = ti('button.save')) - button(label, class: 'btn btn-primary', data: { disable_with: label }) - end + # Render a standard submit button with the given label. + def submit_button(label = ti('button.save')) + button(label, class: 'btn btn-primary', data: { disable_with: label }) + end - # Render a cancel link pointing to the given url. - def cancel_link(url = nil) - url ||= cancel_url - link_to(ti('button.cancel'), url, class: 'cancel') unless url == false - end + # Render a cancel link pointing to the given url. + def cancel_link(url = nil) + url ||= cancel_url + link_to(ti('button.cancel'), url, class: 'cancel') unless url == false + end - # Depending if the given attribute must be present, return - # only an initial selection prompt or a blank option, respectively. - def select_options(attr, options = {}) - prompt = options.delete(:prompt) - blank = options.delete(:include_blank) - if options[:multiple] - {} - elsif prompt - { prompt: prompt } - elsif blank - { include_blank: blank } - else - assoc = association(@object, attr) - if required?(attr) - { prompt: ta(:please_select, assoc) } + # Depending if the given attribute must be present, return + # only an initial selection prompt or a blank option, respectively. + def select_options(attr, options = {}) + prompt = options.delete(:prompt) + blank = options.delete(:include_blank) + if options[:multiple] + {} + elsif prompt + { prompt: } + elsif blank + { include_blank: blank } else - { include_blank: ta(:no_entry, assoc) } + assoc = association(@object, attr) + if required?(attr) + { prompt: ta(:please_select, assoc) } + else + { include_blank: ta(:no_entry, assoc) } + end end end - end - # Returns true if the given attribute must be present. - def required?(attr) - attr, attr_id = assoc_and_id_attr(attr) - validators = @object.class.validators_on(attr) + - @object.class.validators_on(attr_id) - validators.any? do |v| - v.kind == :presence && - !v.options.key?(:if) && - !v.options.key?(:unless) + # Returns true if the given attribute must be present. + def required?(attr) + attr, attr_id = assoc_and_id_attr(attr) + validators = @object.class.validators_on(attr) + + @object.class.validators_on(attr_id) + validators.any? do |v| + v.kind == :presence && + !v.options.key?(:if) && + !v.options.key?(:unless) + end end - end - # Returns true if any errors are found on the passed attribute or its - # association. - def errors?(attr) - attr_plain, attr_id = assoc_and_id_attr(attr) - # errors aint a Hash - @object.errors.key?(attr_plain.to_sym) || - @object.errors.key?(attr_id.to_sym) - # rubocop:enable HashMethods - end + # Returns true if any errors are found on the passed attribute or its + # association. + def errors?(attr) + attr_plain, attr_id = assoc_and_id_attr(attr) + # errors aint a Hash + @object.errors.key?(attr_plain.to_sym) || + @object.errors.key?(attr_id.to_sym) + end - # Render a label for the given attribute with the passed content. - # The content may be given as an argument or as a block: - # labeled(:attr) { #content } - # labeled(:attr, content) - # - # The following options may be passed: - # * :span - Number of columns the content should span. - # * :caption - Different caption for the label. - def labeled(attr, content = {}, options = {}, &block) - if block_given? - options = content - content = capture(&block) - end - control = control_class.new(self, attr, options) - control.render_labeled(content) - end + # Render a label for the given attribute with the passed content. + # The content may be given as an argument or as a block: + # labeled(:attr) { #content } + # labeled(:attr, content) + # + # The following options may be passed: + # * :span - Number of columns the content should span. + # * :caption - Different caption for the label. + def labeled(attr, content = {}, options = {}, &) + if block_given? + options = content + content = capture(&) + end + control = control_class.new(self, attr, options) + control.render_labeled(content) + end - # Dispatch methods starting with 'labeled_' to render a label and the - # corresponding input field. - # E.g. labeled_boolean_field(:checked, class: 'bold') - # To add an additional help text, use the help option. - # E.g. labeled_boolean_field(:checked, help: 'Some Help') - def method_missing(name, *args) - field_method = labeled_field_method?(name) - if field_method - build_labeled_field(field_method, *args) - else - super(name, *args) + # Dispatch methods starting with 'labeled_' to render a label and the + # corresponding input field. + # E.g. labeled_boolean_field(:checked, class: 'bold') + # To add an additional help text, use the help option. + # E.g. labeled_boolean_field(:checked, help: 'Some Help') + def method_missing(name, *) + field_method = labeled_field_method?(name) + if field_method + build_labeled_field(field_method, *) + else + super(name, *) + end end - end - # Overriden to fullfill contract with method_missing 'labeled_' methods. - def respond_to?(name, include_all = false) - labeled_field_method?(name).present? || super(name, include_all) - end + # Overriden to fullfill contract with method_missing 'labeled_' methods. + def respond_to?(name, include_all = false) + labeled_field_method?(name).present? || super(name, include_all) + end + + private - private + # Checks if the passed name corresponds to a field method with a + # 'labeled_' prefix. + def labeled_field_method?(name) + prefix = 'labeled_' + return false unless name.to_s.start_with?(prefix) - # Checks if the passed name corresponds to a field method with a - # 'labeled_' prefix. - def labeled_field_method?(name) - prefix = 'labeled_' - if name.to_s.start_with?(prefix) - field_method = name.to_s[prefix.size..-1] + field_method = name.to_s[prefix.size..] field_method if respond_to?(field_method) end - end - # Renders the corresponding field together with a label, required mark and - # an optional help block. - def build_labeled_field(field_method, *args) - options = args.extract_options! - options[:field_method] = field_method - control_class.new(self, *(args << options)).render_labeled - end - - # Returns the list of association entries, either from options[:list] or - # the instance variable with the pluralized association name. - # Otherwise, if the association defines a #options_list or #list scope, - # this is used to load the entries. - # As a last resort, all entries from the association class are returned. - def association_entries(attr, options) - list = options.delete(:list) - unless list - assoc = association(@object, attr) - list = @template.send(:instance_variable_get, - :"@#{assoc.name.to_s.pluralize}") - list ||= load_association_entries(assoc) - end - list - end + # Renders the corresponding field together with a label, required mark and + # an optional help block. + def build_labeled_field(field_method, *args) + options = args.extract_options! + options[:field_method] = field_method + control_class.new(self, *(args << options)).render_labeled + end - # Automatically load the entries for the given association. - def load_association_entries(assoc) - klass = assoc.klass - list = klass.all - list = list.merge(assoc.scope) if assoc.scope - # Use special scopes if they are defined - if klass.respond_to?(:options_list) - list.options_list - elsif klass.respond_to?(:list) - list.list - else + # Returns the list of association entries, either from options[:list] or + # the instance variable with the pluralized association name. + # Otherwise, if the association defines a #options_list or #list scope, + # this is used to load the entries. + # As a last resort, all entries from the association class are returned. + def association_entries(attr, options) + list = options.delete(:list) + unless list + assoc = association(@object, attr) + list = @template.send(:instance_variable_get, + :"@#{assoc.name.to_s.pluralize}") + list ||= load_association_entries(assoc) + end list end - end - # Get the cancel url for the given object considering options: - # 1. Use :cancel_url_new or :cancel_url_edit option, if present - # 2. Use :cancel_url option, if present - def cancel_url - url = if @object.new_record? - options[:cancel_url_new] - else - options[:cancel_url_edit] - end - url || options[:cancel_url] + # Automatically load the entries for the given association. + def load_association_entries(assoc) + klass = assoc.klass + list = klass.all + list = list.merge(assoc.scope) if assoc.scope + # Use special scopes if they are defined + if klass.respond_to?(:options_list) + list.options_list + elsif klass.respond_to?(:list) + list.list + else + list + end + end + + # Get the cancel url for the given object considering options: + # 1. Use :cancel_url_new or :cancel_url_edit option, if present + # 2. Use :cancel_url option, if present + def cancel_url + url = if @object.new_record? + options[:cancel_url_new] + else + options[:cancel_url_edit] + end + url || options[:cancel_url] + end end end end diff --git a/app/helpers/dry_crud/form/control.rb b/app/helpers/dry_crud/form/control.rb index 42b44a5fb..427471bd8 100644 --- a/app/helpers/dry_crud/form/control.rb +++ b/app/helpers/dry_crud/form/control.rb @@ -1,170 +1,172 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -module DryCrud::Form - # Internal class to handle the rendering of a single form control, - # consisting of a label, input field, addon, help text or - # required mark. - class Control - attr_reader :builder, :attr, :args, :options, :span, :addon, :help - - delegate :content_tag, :object, - to: :builder - - # Html displayed to mark an input as required. - REQUIRED_MARK = '*'.freeze - - # Number of default input field span columns depending - # on the #field_method. - INPUT_SPANS = Hash.new(5) - INPUT_SPANS[:number_field] = - INPUT_SPANS[:integer_field] = - INPUT_SPANS[:float_field] = - INPUT_SPANS[:decimal_field] = 2 - INPUT_SPANS[:date_field] = - INPUT_SPANS[:time_field] = 2 - - # Create a new control instance. - # Takes the form builder, the attribute to build the control for - # as well as any additional arguments for the field method. - # This includes an options hash as the last argument, that - # may contain the following special options: - # - # * :addon - Addon content displayd just after the input field. - # * :help - A help text displayd below the input field. - # * :span - Number of columns the input field should span. - # * :caption - Different caption for the label. - # * :field_method - Different method to create the input field. - # * :required - Sets the field as required - # (The value for this option usually is 'required'). - # - # All the other options will go to the field_method. - def initialize(builder, attr, *args) - @builder = builder - @attr = attr - @options = args.extract_options! - @args = args - - @addon = options.delete(:addon) - @help = options.delete(:help) - @span = options.delete(:span) - @caption = options.delete(:caption) - @field_method = options.delete(:field_method) - @required = options[:required] - end +module DryCrud + module Form + # Internal class to handle the rendering of a single form control, + # consisting of a label, input field, addon, help text or + # required mark. + class Control + attr_reader :builder, :attr, :args, :options, :span, :addon, :help + + delegate :content_tag, :object, + to: :builder + + # Html displayed to mark an input as required. + REQUIRED_MARK = '*' + + # Number of default input field span columns depending + # on the #field_method. + INPUT_SPANS = Hash.new(5) + INPUT_SPANS[:number_field] = + INPUT_SPANS[:integer_field] = + INPUT_SPANS[:float_field] = + INPUT_SPANS[:decimal_field] = 2 + INPUT_SPANS[:date_field] = + INPUT_SPANS[:time_field] = 2 + + # Create a new control instance. + # Takes the form builder, the attribute to build the control for + # as well as any additional arguments for the field method. + # This includes an options hash as the last argument, that + # may contain the following special options: + # + # * :addon - Addon content displayd just after the input field. + # * :help - A help text displayd below the input field. + # * :span - Number of columns the input field should span. + # * :caption - Different caption for the label. + # * :field_method - Different method to create the input field. + # * :required - Sets the field as required + # (The value for this option usually is 'required'). + # + # All the other options will go to the field_method. + def initialize(builder, attr, *args) + @builder = builder + @attr = attr + @options = args.extract_options! + @args = args + + @addon = options.delete(:addon) + @help = options.delete(:help) + @span = options.delete(:span) + @caption = options.delete(:caption) + @field_method = options.delete(:field_method) + @required = options[:required] + end - # Renders only the content of the control. - # I.e. no label and span divs. - def render_content - content - end + # Renders only the content of the control. + # I.e. no label and span divs. + def render_content + content + end - # Renders the complete control with label and everything. - # Render the content given or the default one. - def render_labeled(content = nil) - @content = content if content - labeled - end + # Renders the complete control with label and everything. + # Render the content given or the default one. + def render_labeled(content = nil) + @content = content if content + labeled + end - private + private - # Create the HTML markup for any labeled content. - def labeled - errors = builder.errors?(attr) ? ' has-error' : '' + # Create the HTML markup for any labeled content. + def labeled + errors = builder.errors?(attr) ? ' has-error' : '' - content_tag(:div, class: "form-group#{errors}") do - label_class = "col-md-2 control-label #{'required' if required}" - html = builder.label(attr, caption, class: label_class) - html << content_tag(:div, content, class: "col-md-#{span}") - if help.present? - html << builder.help_block(help) + content_tag(:div, class: "form-group#{errors}") do + label_class = "col-md-2 control-label #{'required' if required}" + html = builder.label(attr, caption, class: label_class) + html << content_tag(:div, content, class: "col-md-#{span}") + html << builder.help_block(help) if help.present? + html end - html end - end - # Return the currently set content or create it - # based on the various options given. - # - # Optionally renders addon, required mark and/or a help block - # additionally to the input field. - def content - @content ||= begin - content = input - content = builder.with_addon(content, addon) if addon - content + # Return the currently set content or create it + # based on the various options given. + # + # Optionally renders addon, required mark and/or a help block + # additionally to the input field. + def content + @content ||= begin + content = input + content = builder.with_addon(content, addon) if addon + content + end end - end - # Return the currently set input field or create it - # depending on the attribute. - def input - @input ||= begin - options[:required] = 'required' if required - builder.send(field_method, attr, *(args << options)) + # Return the currently set input field or create it + # depending on the attribute. + def input + @input ||= begin + options[:required] = 'required' if required + builder.send(field_method, attr, *(args << options)) + end end - end - # The field method used to create the input. - # If none is set, detect it from the attribute type. - def field_method - @field_method ||= detect_field_method - end + # The field method used to create the input. + # If none is set, detect it from the attribute type. + def field_method + @field_method ||= detect_field_method + end - # True if the attr is required, false otherwise. - def required - @required = @required.nil? ? builder.required?(attr) : @required - end + # True if the attr is required, false otherwise. + def required + @required = @required.nil? ? builder.required?(attr) : @required + end - # Number of grid columns the input field should span. - def span - @span ||= INPUT_SPANS[field_method.to_sym] - end + # Number of grid columns the input field should span. + def span + @span ||= INPUT_SPANS[field_method.to_sym] + end - # The caption of the label. - # If none is set, uses the I18n value of the attribute. - def caption - @caption ||= builder.captionize(attr, object.class) - end + # The caption of the label. + # If none is set, uses the I18n value of the attribute. + def caption + @caption ||= builder.captionize(attr, object.class) + end - # Defines the field method to use based on the attribute - # type, association or name. - def detect_field_method - if type == :text - :text_area - elsif association_kind?(:belongs_to) - :belongs_to_field - elsif association_kind?(:has_and_belongs_to_many, :has_many) - :has_many_field - elsif attr.to_s.include?('password') - :password_field - elsif attr.to_s.include?('email') - :email_field - elsif object.class.defined_enums.key?(attr.to_s) - :enum_field - elsif builder.respond_to?(:"#{type}_field") - :"#{type}_field" - else - :text_field + # Defines the field method to use based on the attribute + # type, association or name. + def detect_field_method + if type == :text + :text_area + elsif association_kind?(:belongs_to) + :belongs_to_field + elsif association_kind?(:has_and_belongs_to_many, :has_many) + :has_many_field + elsif attr.to_s.include?('password') + :password_field + elsif attr.to_s.include?('email') + :email_field + elsif object.class.defined_enums.key?(attr.to_s) + :enum_field + elsif builder.respond_to?(:"#{type}_field") + :"#{type}_field" + else + :text_field + end end - end - # The column type of the attribute. - def type - @type ||= builder.column_type(object, attr) - end + # The column type of the attribute. + def type + @type ||= builder.column_type(object, attr) + end - # Returns true if attr is a non-polymorphic association. - # If one or more macros are given, the association must be of this kind. - def association_kind?(*macros) - if type == :integer || type.nil? - assoc = builder.association(object, attr, *macros) + # Returns true if attr is a non-polymorphic association. + # If one or more macros are given, the association must be of this kind. + def association_kind?(*macros) + if type == :integer || type.nil? + assoc = builder.association(object, attr, *macros) - assoc.present? && assoc.options[:polymorphic].nil? - else - false + assoc.present? && assoc.options[:polymorphic].nil? + else + false + end end end end diff --git a/app/helpers/dry_crud/table/actions.rb b/app/helpers/dry_crud/table/actions.rb index 6108610dd..3178b8979 100644 --- a/app/helpers/dry_crud/table/actions.rb +++ b/app/helpers/dry_crud/table/actions.rb @@ -1,81 +1,83 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -module DryCrud::Table - # Adds action columns to the table builder. - # Predefined actions are available for show, edit and destroy. - # Additionally, a special col type to define cells linked to the show page - # of the row entry is provided. - module Actions - extend ActiveSupport::Concern +module DryCrud + module Table + # Adds action columns to the table builder. + # Predefined actions are available for show, edit and destroy. + # Additionally, a special col type to define cells linked to the show page + # of the row entry is provided. + module Actions + extend ActiveSupport::Concern - included do - delegate :link_to, :link_to_if, :path_args, :polymorphic_path, :edit_polymorphic_path, - :ti, :picon, :can?, - to: :template - end + included do + delegate :link_to, :link_to_if, :path_args, :polymorphic_path, :edit_polymorphic_path, + :ti, :picon, :can?, + to: :template + end - # Renders the passed attr with a link to the show action for - # the current entry. - # A block may be given to define the link path for the row entry. - def attr_with_show_link(attr, &_block) - sortable_attr(attr) do |e| - path = path_args(e) - if can?(:edit, e) - link_to(format_attr(e, attr), edit_polymorphic_path(path)) - elsif can?(:show, e) - link_to(format_attr(e, attr), polymorphic_path(path)) - else - format_attr(e, attr) + # Renders the passed attr with a link to the show action for + # the current entry. + # A block may be given to define the link path for the row entry. + def attr_with_show_link(attr, &) + sortable_attr(attr) do |e| + path = path_args(e) + if can?(:edit, e) + link_to(format_attr(e, attr), edit_polymorphic_path(path)) + elsif can?(:show, e) + link_to(format_attr(e, attr), polymorphic_path(path)) + else + format_attr(e, attr) + end end end - end - # Action column to show the row entry. - # A block may be given to define the link path for the row entry. - # If the block returns nil, no link is rendered. - def show_action_col(html_options = {}, &_block) - action_col do |e| - link_to_if(can?(:show, e), 'Anzeigen', path_args(e), html_options.clone) + # Action column to show the row entry. + # A block may be given to define the link path for the row entry. + # If the block returns nil, no link is rendered. + def show_action_col(html_options = {}, &) + action_col do |e| + link_to_if(can?(:show, e), 'Anzeigen', path_args(e), html_options.clone) + end end - end - # Action column to edit the row entry. - # A block may be given to define the link path for the row entry. - # If the block returns nil, no link is rendered. - def edit_action_col(html_options = {}, &_block) - html_options = html_options.merge(title: 'Bearbeiten') - action_col do |e| - path = path_args(e) - if can?(:edit, e) - table_action_link('edit', edit_polymorphic_path(path), html_options) + # Action column to edit the row entry. + # A block may be given to define the link path for the row entry. + # If the block returns nil, no link is rendered. + def edit_action_col(html_options = {}, &) + html_options = html_options.merge(title: 'Bearbeiten') + action_col do |e| + path = path_args(e) + table_action_link('edit', edit_polymorphic_path(path), html_options) if can?(:edit, e) end end - end - # Action column to destroy the row entry. - # A block may be given to define the link path for the row entry. - # If the block returns nil, no link is rendered. - def destroy_action_col(html_options = {}, &_block) - html_options = html_options.merge(title: 'Löschen', - data: { confirm: ti(:confirm_delete), - method: :delete }) - action_col do |e| - table_action_link('delete', path_args(e), html_options) if can?(:destroy, e) + # Action column to destroy the row entry. + # A block may be given to define the link path for the row entry. + # If the block returns nil, no link is rendered. + def destroy_action_col(html_options = {}, &) + html_options = html_options.merge(title: 'Löschen', + data: { confirm: ti(:confirm_delete), + method: :delete }) + action_col do |e| + table_action_link('delete', path_args(e), html_options) if can?(:destroy, e) + end end - end - # Action column inside a table. No header. - # The cell content should be defined in the passed block. - def action_col(&block) - col('', class: 'action', &block) - end + # Action column inside a table. No header. + # The cell content should be defined in the passed block. + def action_col(&) + col('', class: 'action', &) + end - # Generic action link inside a table. - def table_action_link(icon, url, html_options = {}) - link_to(picon(icon), url, html_options) + # Generic action link inside a table. + def table_action_link(icon, url, html_options = {}) + link_to(picon(icon), url, html_options) + end end end end diff --git a/app/helpers/dry_crud/table/builder.rb b/app/helpers/dry_crud/table/builder.rb index ec7d68a11..e3a8c04de 100644 --- a/app/helpers/dry_crud/table/builder.rb +++ b/app/helpers/dry_crud/table/builder.rb @@ -1,126 +1,130 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -module DryCrud::Table - # A simple helper to easily define tables listing several rows of the same - # data type. - # - # Example Usage: - # DryCrud::Table::Builder.table(entries, template) do |t| - # t.col('My Header', class: 'css') {|e| link_to 'Show', e } - # t.attrs :name, :city - # end - class Builder - include Sorting - include Actions - - attr_reader :entries, :cols, :options, :template - - delegate :content_tag, :format_attr, :column_type, :association, :dom_id, - :captionize, :add_css_class, :content_tag_nested, - to: :template - - def initialize(entries, template, options = {}) - @entries = entries - @template = template - @options = options - @footer = nil - @cols = [] - end +module DryCrud + module Table + # A simple helper to easily define tables listing several rows of the same + # data type. + # + # Example Usage: + # DryCrud::Table::Builder.table(entries, template) do |t| + # t.col('My Header', class: 'css') {|e| link_to 'Show', e } + # t.attrs :name, :city + # end + class Builder + include Sorting + include Actions + + attr_reader :entries, :cols, :options, :template + + delegate :content_tag, :format_attr, :column_type, :association, :dom_id, + :captionize, :add_css_class, :content_tag_nested, + to: :template + + def initialize(entries, template, options = {}) + @entries = entries + @template = template + @options = options + @footer = nil + @cols = [] + end - # Convenience method to directly generate a table. Renders a row for each - # entry in entries. Takes a block that gets the table object as parameter - # for configuration. Returns the generated html for the table. - def self.table(entries, template, options = {}) - t = new(entries, template, options) - yield t - t.to_html - end + # Convenience method to directly generate a table. Renders a row for each + # entry in entries. Takes a block that gets the table object as parameter + # for configuration. Returns the generated html for the table. + def self.table(entries, template, options = {}) + t = new(entries, template, options) + yield t + t.to_html + end - # Define a column for the table with the given header, the html_options - # used for each td and a block rendering the contents of a cell for the - # current entry. The columns appear in the order they are defined. - def col(header = '', html_options = {}, &block) - @cols << Col.new(header, html_options, @template, block) - end + # Define a column for the table with the given header, the html_options + # used for each td and a block rendering the contents of a cell for the + # current entry. The columns appear in the order they are defined. + def col(header = '', html_options = {}, &block) + @cols << Col.new(header, html_options, @template, block) + end - # Convenience method to add one or more attribute columns. - # The attribute name will become the header, the cells will contain - # the formatted attribute value for the current entry. - def attrs(*attrs) - attrs.each do |a| - attr(a) + # Convenience method to add one or more attribute columns. + # The attribute name will become the header, the cells will contain + # the formatted attribute value for the current entry. + def attrs(*attrs) + attrs.each do |a| + attr(a) + end end - end - # Define a column for the given attribute and an optional header. - # If no header is given, the attribute name is used. The cell will - # contain the formatted attribute value for the current entry. - def attr(a, header = nil, html_options = {}, &block) - header ||= attr_header(a) - block ||= ->(e) { format_attr(e, a) } - add_css_class(html_options, align_class(a)) - col(header, html_options, &block) - end + # Define a column for the given attribute and an optional header. + # If no header is given, the attribute name is used. The cell will + # contain the formatted attribute value for the current entry. + def attr(a, header = nil, html_options = {}, &block) + header ||= attr_header(a) + block ||= ->(e) { format_attr(e, a) } + add_css_class(html_options, align_class(a)) + col(header, html_options, &block) + end - def row_attrs(&block) - @row_attrs = block - end + def row_attrs(&block) + @row_attrs = block + end - # Renders the table as HTML. - def to_html - content_tag :table, options do - content_tag(:thead, html_header) + - content_tag_nested(:tbody, entries) { |e| html_row(e) } + - (@footer ? content_tag(:tfoot, @footer) : '') + # Renders the table as HTML. + def to_html + content_tag :table, options do + content_tag(:thead, html_header) + + content_tag_nested(:tbody, entries) { |e| html_row(e) } + + (@footer ? content_tag(:tfoot, @footer) : '') + end end - end - # Returns css classes used for alignment of the cell data. - # Based on the column type of the attribute. - def align_class(attr) - entry = entries.present? ? entry_class.new : nil - case column_type(entry, attr) - when :integer, :float, :decimal, :date, :time, :datetime, :timestamp - 'text-right' unless association(entry, attr, :belongs_to) - when :boolean - 'text-center' + # Returns css classes used for alignment of the cell data. + # Based on the column type of the attribute. + def align_class(attr) + entry = entries.present? ? entry_class.new : nil + case column_type(entry, attr) + when :integer, :float, :decimal, :date, :time, :datetime, :timestamp + 'text-right' unless association(entry, attr, :belongs_to) + when :boolean + 'text-center' + end end - end - # Creates a header string for the given attr. - def attr_header(attr) - captionize(attr, entry_class) - end + # Creates a header string for the given attr. + def attr_header(attr) + captionize(attr, entry_class) + end - # Create a table footer with arbitrary html - def foot(&block) - @footer = template.capture(entries, &block) - end + # Create a table footer with arbitrary html + def foot(&) + @footer = template.capture(entries, &) + end - private + private - # Renders the header row of the table. - def html_header - content_tag_nested(:tr, cols, &:html_header) - end + # Renders the header row of the table. + def html_header + content_tag_nested(:tr, cols, &:html_header) + end - # Renders a table row for the given entry. - def html_row(entry) - attrs = @row_attrs ? @row_attrs.call(entry) : {} - attrs[:id] ||= dom_id(entry) if entry.respond_to?(:to_key) - content_tag_nested(:tr, cols, attrs) { |c| c.html_cell(entry, options[:responsive]) } - end + # Renders a table row for the given entry. + def html_row(entry) + attrs = @row_attrs ? @row_attrs.call(entry) : {} + attrs[:id] ||= dom_id(entry) if entry.respond_to?(:to_key) + content_tag_nested(:tr, cols, attrs) { |c| c.html_cell(entry, options[:responsive]) } + end - # Determines the class of the table entries. - # All entries should be of the same type. - def entry_class - if entries.respond_to?(:klass) - entries.klass - else - entries.first.class + # Determines the class of the table entries. + # All entries should be of the same type. + def entry_class + if entries.respond_to?(:klass) + entries.klass + else + entries.first.class + end end end end diff --git a/app/helpers/dry_crud/table/col.rb b/app/helpers/dry_crud/table/col.rb index 8cd317923..81ea2ffe5 100644 --- a/app/helpers/dry_crud/table/col.rb +++ b/app/helpers/dry_crud/table/col.rb @@ -1,34 +1,38 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -module DryCrud::Table - # Helper class to store column information. - class Col < Struct.new(:header, :html_options, :template, :block) #:nodoc: - delegate :content_tag, :capture, to: :template +module DryCrud + module Table + # Helper class to store column information. + Col = Struct.new(:header, :html_options, :template, :block) do # :nodoc: + delegate :content_tag, :capture, to: :template - # Runs the Col block for the given entry. - def content(entry) - entry.nil? ? '' : capture(entry, &block) - end + # Runs the Col block for the given entry. + def content(entry) + entry.nil? ? '' : capture(entry, &block) + end - # Renders the header cell of the Col. - def html_header - content_tag(:th, header, html_options) - end + # Renders the header cell of the Col. + def html_header + content_tag(:th, header, html_options) + end - # Renders a table cell for the given entry. - def html_cell(entry, responsive = false) - output = content(entry) + # Renders a table cell for the given entry. + def html_cell(entry, responsive = false) + output = content(entry) - if responsive - options = header.present? ? { data: { label: header } } : {} - template.add_css_class html_options, 'empty-cell' if output.blank? - html_options.merge!(options) - end + if responsive + options = header.present? ? { data: { label: header } } : {} + template.add_css_class html_options, 'empty-cell' if output.blank? + html_options.merge!(options) + end - content_tag(:td, output, html_options) + content_tag(:td, output, html_options) + end end end end diff --git a/app/helpers/dry_crud/table/sorting.rb b/app/helpers/dry_crud/table/sorting.rb index ca020f86b..cf8f72cb8 100644 --- a/app/helpers/dry_crud/table/sorting.rb +++ b/app/helpers/dry_crud/table/sorting.rb @@ -1,33 +1,40 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -module DryCrud::Table - # Provides headers with sort links. Expects a method :sortable?(attr) - # in the template/controller to tell if an attribute is sortable or not. - # Extracted into an own module for convenience. - module Sorting - # Create a header with sort links and a mark for the current sort - # direction. - def sort_header(attr, label = nil) - label ||= attr_header(attr) - template.sort_link(attr, label) - end +module DryCrud + module Table + # Provides headers with sort links. Expects a method :sortable?(attr) + # in the template/controller to tell if an attribute is sortable or not. + # Extracted into an own module for convenience. + module Sorting + # Create a header with sort links and a mark for the current sort + # direction. + def sort_header(attr, label = nil) + label ||= attr_header(attr) + template.sort_link(attr, label) + end - # Same as :attrs, except that it renders a sort link in the header - # if an attr is sortable. - def sortable_attrs(*attrs) - attrs.each { |a| sortable_attr(a) } - end + # Same as :attrs, except that it renders a sort link in the header + # if an attr is sortable. + def sortable_attrs(*attrs) + attrs.each { |a| sortable_attr(a) } + end - # Renders a sort link header, otherwise similar to :attr. - def sortable_attr(a, header = nil, html_options = {}, &block) - if template.sortable?(a) - attr(a, sort_header(a, header), html_options, &block) - else - attr(a, header, html_options, &block) + # Disable the attr detection from rubocop, because this calls our attr function. + # rubocop:disable Style/Attr + # Renders a sort link header, otherwise similar to :attr. + def sortable_attr(a, header = nil, html_options = {}, &) + if template.sortable?(a) + attr(a, sort_header(a, header), html_options, &) + else + attr(a, header, html_options, &) + end end + # rubocop:enable Style/Attr end end end diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb new file mode 100644 index 000000000..6f67e2fe5 --- /dev/null +++ b/app/helpers/email_helper.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module EmailHelper + def email_image_tag(image, **) + attachments.inline[image] = Rails.root.join("app/assets/images/#{image}").read + image_tag(attachments[image].url, **) + end +end diff --git a/app/helpers/employee_master_data_helper.rb b/app/helpers/employee_master_data_helper.rb index 009241538..f772b79eb 100644 --- a/app/helpers/employee_master_data_helper.rb +++ b/app/helpers/employee_master_data_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -5,11 +7,15 @@ module EmployeeMasterDataHelper def format_year_of_service(employment_date) - ((Time.zone.now - employment_date.to_time) / 1.year.seconds).floor + start = employment_date.to_time.to_i + now = DateTime.now.to_i + duration = ActiveSupport::Duration.build(now - start) + + duration.parts[:years] end def format_nationalities(employee) - return unless employee.nationalities.present? + return if employee.nationalities.blank? employee.nationalities.map do |country_code| country = ISO3166::Country[country_code] diff --git a/app/helpers/employees_helper.rb b/app/helpers/employees_helper.rb index eaad15566..22c14d4f8 100644 --- a/app/helpers/employees_helper.rb +++ b/app/helpers/employees_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -14,7 +16,7 @@ def format_employee_current_percent(employee) end def multiple_check_box(object_name, field, value) - object = instance_variable_get("@#{object_name}") + object = instance_variable_get(:"@#{object_name}") check_box_tag "#{object_name}[#{field}][]", value, object.send(field).include?(value) end diff --git a/app/helpers/employments_helper.rb b/app/helpers/employments_helper.rb index 3b33f223d..bd82563f1 100644 --- a/app/helpers/employments_helper.rb +++ b/app/helpers/employments_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/helpers/evaluator_helper.rb b/app/helpers/evaluator_helper.rb index 62d71d00c..9bd2bb3b1 100644 --- a/app/helpers/evaluator_helper.rb +++ b/app/helpers/evaluator_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -5,11 +7,11 @@ module EvaluatorHelper def evaluation_detail_params - params.permit(:evaluation, :category_id, :division_id, :start_date, :end_date, :page) + params.permit(:evaluation, :category_id, :division_id, :absence_id, :start_date, :end_date, :page) end def evaluation_path(evaluation, options = {}) - url_for(options.merge(controller: '/evaluator', action: 'overview', evaluation: evaluation)) + url_for(options.merge(controller: '/evaluator', action: 'overview', evaluation:)) end def detail_th_align(field) @@ -75,10 +77,10 @@ def remaining_vacations(employee) end def overtime_vacations_tooltip(employee) - transfers = employee.overtime_vacations. - where(@period ? ['transfer_date <= ?', @period.end_date] : nil). - order('transfer_date'). - to_a + transfers = employee.overtime_vacations + .where(@period ? ['transfer_date <= ?', @period.end_date] : nil) + .order('transfer_date') + .to_a tooltip = '' unless transfers.empty? tooltip = '<->Überstunden-Ferien Umbuchungen:
' @@ -163,7 +165,7 @@ def employment_role_infos(employment) .order('percent DESC') .map do |ere| role = ere.employment_role.name - role += ' ' + ere.employment_role_level.name if ere.employment_role_level.present? + role += " #{ere.employment_role_level.name}" if ere.employment_role_level.present? [role, ere.percent] end end diff --git a/app/helpers/expenses_helper.rb b/app/helpers/expenses_helper.rb index 4c9bb77e5..4899e2a72 100644 --- a/app/helpers/expenses_helper.rb +++ b/app/helpers/expenses_helper.rb @@ -2,7 +2,7 @@ module ExpensesHelper def format_expense_status_value(expense) - memo = Expense.statuses.keys.zip(%w(info warning success danger)).to_h + memo = Expense.statuses.keys.zip(%w[info warning success danger]).to_h content_tag(:span, expense.status_value, class: "label label-#{memo[expense.status]}") end @@ -14,7 +14,7 @@ def expense_details_col(table, personal: true) table.col('', class: 'right') do |e| path = personal ? employee_expense_path(e.employee, e) : expense_path(e) link_to(path, title: 'Details') do - tag.i(class: 'icon-document') + ' Details' + "#{tag.i(class: 'icon-document')} Details".html_safe end end end @@ -64,7 +64,7 @@ def expense_review_col(table) if e.pending? || e.deferred? link_to(expenses_review_path(e), title: 'Kontrollieren') do - tag.i(class: 'icon-edit') + ' Kontrollieren' + "#{tag.i(class: 'icon-edit')} Kontrollieren".html_safe end end @@ -121,7 +121,7 @@ def expenses_order_field(form, **options) def expenses_file_field(form, **options) safe_join( [ - file_field_with_warning(form, options), + file_field_with_warning(form, **options), form.labeled(' ', options) { t('expenses.attachment.hint') } ] ) @@ -133,7 +133,7 @@ def file_field_with_warning(form, **options) # Due to inconsistent browser behaviour, we need both file endings # and the 'all images' mimetype options.deep_merge!( - accept: %w(image/gif image/jpeg image/png .gif .jpg .jpeg .png).join(','), + accept: %w[image/gif image/jpeg image/png .gif .jpg .jpeg .png].join(','), required: !entry.receipt.attached? ) diff --git a/app/helpers/filter_helper.rb b/app/helpers/filter_helper.rb index eeacd9969..f2d7b7103 100644 --- a/app/helpers/filter_helper.rb +++ b/app/helpers/filter_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -33,10 +35,10 @@ def order_uncertainty_options IdValue.new(:high, 'gross')] end - def direct_filter(name, label, &block) + def direct_filter(name, label, &) html = ''.html_safe html += label_tag(name, label, class: 'control-label') + '   '.html_safe if label - html += capture(&block) + html += capture(&) content_tag(:div, html, class: 'form-group') end diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index cdf00075c..b610b1131 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -12,7 +14,7 @@ # attributes or default. module FormHelper # Renders a form using Crud::FormBuilder. - def plain_form(object, options = {}, &block) + def plain_form(object, options = {}, &) options[:html] ||= {} add_css_class(options[:html], 'form-horizontal') options[:html][:role] ||= 'form' @@ -23,7 +25,7 @@ def plain_form(object, options = {}, &block) options[:data][:remote] = true end - form_for(object, options, &block) + form_for(object, options, &) end # Renders a standard form for the given entry and attributes. @@ -35,7 +37,7 @@ def standard_form(object, *attrs, &block) plain_form(object, attrs.extract_options!) do |form| content = form.error_messages - content << if block_given? + content << if block capture(form, &block) else form.labeled_input_fields(*attrs) @@ -49,11 +51,11 @@ def standard_form(object, *attrs, &block) # Renders a crud form for the current entry with default_crud_attrs or the # given attribute array. An options hash may be given as the last argument. # If a block is given, a custom form may be rendered and attrs is ignored. - def crud_form(*attrs, &block) + def crud_form(*attrs, &) options = attrs.extract_options! - attrs = default_crud_attrs - [:created_at, :updated_at] if attrs.blank? + attrs = default_crud_attrs - %i[created_at updated_at] if attrs.blank? attrs << options - standard_form(path_args(entry), *attrs, &block) + standard_form(path_args(entry), *attrs, &) end def spinner diff --git a/app/helpers/format_helper.rb b/app/helpers/format_helper.rb index c609afe38..55c49ba50 100644 --- a/app/helpers/format_helper.rb +++ b/app/helpers/format_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -14,10 +16,10 @@ module FormatHelper # Formats a basic value based on its Ruby class. def f(value) case value - when Float, BigDecimal then + when Float, BigDecimal number_with_precision(value, precision: t('number.format.precision'), delimiter: t('number.format.delimiter')) - when Integer then + when Integer number_with_delimiter(value, delimiter: t('number.format.delimiter')) when Date then l(value, format: :long) when Time then "#{l(value.to_date)} #{l(value, format: :time)}" @@ -41,7 +43,7 @@ def format_attr(obj, attr) end def format_number(number, precision = 2) - number_with_precision(number, precision: precision, delimiter: '\'') + number_with_precision(number, precision:, delimiter: '\'') end def format_hour(hour, precision = 2) @@ -54,10 +56,10 @@ def format_time(time) end def format_day(date, full_weekday_name = false) - if date - format = full_weekday_name ? '%A, %e.%-m.' : '%a %e.%-m.' - I18n.l(date, format: format) - end + return unless date + + format = full_weekday_name ? '%A, %e.%-m.' : '%a %e.%-m.' + I18n.l(date, format:) end def localize_date(date) @@ -73,7 +75,7 @@ def format_days(number, short = false) end def format_percent(value) - (value == value.to_i ? value.to_i.to_s : value.to_s) + ' %' + "#{value == value.to_i ? value.to_i.to_s : value.to_s} %" end def format_expense_employee_id(value) @@ -87,7 +89,7 @@ def format_expense_reviewer_id(value) # Renders a simple unordered list, which will # simply render all passed items or yield them # to your block. - def simple_list(items, ul_options = {}, &_block) + def simple_list(items, ul_options = {}, &) content_tag_nested(:ul, items, ul_options) do |item| content_tag(:li, block_given? ? yield(item) : f(item)) end @@ -108,9 +110,9 @@ def labeled_attr(obj, attr) # Renders an arbitrary content with the given label. Used for uniform # presentation. - def labeled(label, content = nil, &block) - content = capture(&block) if block_given? - render('shared/labeled', label: label, content: content) + def labeled(label, content = nil, &) + content = capture(&) if block_given? + render('shared/labeled', label:, content:) end # Transform the given text into a form as used by labels or table headers. @@ -167,8 +169,8 @@ def format_type(obj, attr) when :time then l(val, format: :time) when :date then f(val.to_date) when :datetime, :timestamp then f(val.time) - when :text then simple_format(h(val)) - when :decimal then + when :text then simple_format(h(val)) + when :decimal number_with_precision(val.to_s.to_f, precision: column_property(obj, attr, :scale), delimiter: t('number.format.delimiter')) @@ -214,7 +216,7 @@ def assoc_link(val) def show_path_exists?(path_method, val) Rails.application.routes.recognize_path(send(path_method, val), method: :get) true - rescue + rescue StandardError false end end diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb index cc3e3239c..db70548b3 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -81,7 +83,7 @@ def timebox_div(box) end end - def worktime_link(worktime, &block) + def worktime_link(worktime, &) url = if worktime && !worktime.new_record? if can?(:edit, worktime) url_for(controller: worktime.controller, @@ -93,10 +95,10 @@ def worktime_link(worktime, &block) week_date: worktime.work_date) end end - content_tag(:a, class: 'has-tooltip', href: url, &block) + content_tag(:a, class: 'has-tooltip', href: url, &) end - def day_td(date, &block) - content_tag(:td, class: ('holiday' if Holiday.non_working_day?(date)), &block) + def day_td(date, &) + content_tag(:td, class: ('holiday' if Holiday.non_working_day?(date)), &) end end diff --git a/app/helpers/i18n_helper.rb b/app/helpers/i18n_helper.rb index 47d884136..b51476b56 100644 --- a/app/helpers/i18n_helper.rb +++ b/app/helpers/i18n_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -17,10 +19,10 @@ module I18nHelper # - ... # - global.{key} def translate_inheritable(key, variables = {}) - partial = defined?(@virtual_path) ? @virtual_path.gsub(%r(.*\/_?), '') : nil + partial = defined?(@virtual_path) ? @virtual_path.gsub(%r{.*/_?}, '') : nil defaults = inheritable_translation_defaults(key, partial) variables[:default] ||= defaults - t(defaults.shift, variables) + t(defaults.shift, **variables) end alias ti translate_inheritable @@ -36,9 +38,9 @@ def translate_association(key, assoc = nil, variables = {}) if assoc && assoc.options[:polymorphic].nil? variables[:default] ||= [association_klass_key(assoc, key).to_sym, :"global.associations.#{key}"] - t(association_owner_key(assoc, key), variables) + t(association_owner_key(assoc, key), **variables) else - t("global.associations.#{key}", variables) + t("global.associations.#{key}", **variables) end end @@ -48,21 +50,15 @@ def translate_association(key, assoc = nil, variables = {}) # General translation key based on the klass of the association. def association_klass_key(assoc, key) - k = 'activerecord.associations.' - k << assoc.klass.model_name.singular - k << '.' - k << key.to_s + model_name = assoc.klass.model_name.singular + "activerecord.associations.#{model_name}.#{key}" end # Specific translation key based on the owner model and the name # of the association. def association_owner_key(assoc, key) - k = 'activerecord.associations.models.' - k << assoc.active_record.model_name.singular - k << '.' - k << assoc.name.to_s - k << '.' - k << key.to_s + model_name = assoc.active_record.model_name.singular + "activerecord.associations.models.#{model_name}.#{assoc.name}.#{key}" end def inheritable_translation_defaults(key, partial) @@ -70,9 +66,7 @@ def inheritable_translation_defaults(key, partial) current = controller.class while current < ActionController::Base folder = current.controller_path - if folder.present? - append_controller_translation_keys(defaults, folder, partial, key) - end + append_controller_translation_keys(defaults, folder, partial, key) if folder.present? current = current.superclass end defaults << :"global.#{key}" diff --git a/app/helpers/invoice_helper.rb b/app/helpers/invoice_helper.rb index 958bc6043..cc49ff726 100644 --- a/app/helpers/invoice_helper.rb +++ b/app/helpers/invoice_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -16,7 +18,7 @@ def render_address(address) end def format_invoice_calculated_total_amount(entry) - f(entry.calculated_total_amount) + ' ' + Settings.defaults.currency + "#{f(entry.calculated_total_amount)} #{Settings.defaults.currency}" end def format_billing_date(entry) diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb index 2cb52b7d0..655fe488c 100644 --- a/app/helpers/javascript_helper.rb +++ b/app/helpers/javascript_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -8,7 +10,7 @@ def modal_create_link(path, element, title, options = {}) options[:id] ||= "#{element}_create_link" options[:data] ||= {} options[:data].merge!(modal: '#modal', - title: title, + title:, element: "##{element}", remote: true, type: :html, diff --git a/app/helpers/meal_compensations_helper.rb b/app/helpers/meal_compensations_helper.rb index b54e3b8ab..8e635bf09 100644 --- a/app/helpers/meal_compensations_helper.rb +++ b/app/helpers/meal_compensations_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -13,8 +15,8 @@ def meal_compensation_positions(worktimes) def employee_meal_compensations(worktimes) compacted_worktime(worktimes).map do |employee, workdates| - numb_of_days = workdates.values.sum(0) { |h| h >= 4 ? 1 : 0 } - next if numb_of_days == 0 + numb_of_days = workdates.values.sum { |h| h >= 4 ? 1 : 0 } + next if numb_of_days.zero? yield(employee, numb_of_days) end @@ -27,7 +29,7 @@ def employee_meal_compensation_days(employee, period) dates[worktime.work_date] += worktime.hours dates end - workdates.values.sum(0) { |h| h >= 4 ? 1 : 0 } + workdates.values.sum { |h| h >= 4 ? 1 : 0 } end def commited_state_cell(employee, period) @@ -35,7 +37,7 @@ def commited_state_cell(employee, period) date = format_month(employee.committed_worktimes_at) id = "committed_worktimes_at_#{employee.id}" - content_tag(:span, icon << ' ' << date, id: id) + content_tag(:span, icon << ' ' << date, id:) end def reviewed_state_cell(employee, period) @@ -43,7 +45,7 @@ def reviewed_state_cell(employee, period) date = format_month(employee.reviewed_worktimes_at) id = "reviewed_worktimes_at_#{employee.id}" - content_tag(:span, icon << ' ' << date, id: id) + content_tag(:span, icon << ' ' << date, id:) end def completion_state_icon(state) diff --git a/app/helpers/multi_worktimes_helper.rb b/app/helpers/multi_worktimes_helper.rb index cf97d6fef..f0ffd3e2c 100644 --- a/app/helpers/multi_worktimes_helper.rb +++ b/app/helpers/multi_worktimes_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/helpers/navigation_helper.rb b/app/helpers/navigation_helper.rb index 2b47c6a54..c51e1fa99 100644 --- a/app/helpers/navigation_helper.rb +++ b/app/helpers/navigation_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -9,24 +11,33 @@ module NavigationHelper # the corresponding item is active. # If no active_for are given, the item is only active if the # link url equals the request url. - def nav(label, url, *active_for) + # Also you can give it an exception url, that let's you ignore + # a specific url that would otherwise match. + def nav(label, url, *active_for, except: '') options = active_for.extract_options! - content_tag(:li, link_to(label, url, options), class: nav_active_class(url, active_for)) + active_class = nav_active_class(url, active_for, except) + + content_tag( + :li, + link_to(label, url, options), + class: active_class + ) end def model_nav(model) - if can?(:read, model) - path = polymorphic_path(model) - nav(model.model_name.human(count: 2), path, path) - end + return unless can?(:read, model) + + path = polymorphic_path(model) + nav(model.model_name.human(count: 2), path, path) end private - def nav_active_class(url, active_for) - if current_page?(url) || - active_for.any? { |p| request.path =~ %r{^#{p}} } - 'active' - end + def nav_active_class(url, active_for, except) + return 'active' if current_page?(url) + return if current_page?(except) + return unless active_for.any? { |p| request.path =~ /^#{p}/ } + + 'active' end end diff --git a/app/helpers/omniauth_callbacks_helper.rb b/app/helpers/omniauth_callbacks_helper.rb index 5b085c337..3b6f44920 100644 --- a/app/helpers/omniauth_callbacks_helper.rb +++ b/app/helpers/omniauth_callbacks_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OmniauthCallbacksHelper def sign_in_user(authentication) sign_in_and_redirect Employee.find(authentication.employee_id) @@ -9,8 +11,8 @@ def add_new_oauth(_authentication, omni) current_user.authentications.create!( provider: omni['provider'], uid: omni['uid'], - token: token, - token_secret: token_secret + token:, + token_secret: ) sign_in_and_redirect current_user end @@ -21,7 +23,7 @@ def login_with_matching_data(omni) sign_in_and_redirect employee else ldapname = login_fields(omni)[:ldapname] - flash[:alert] = t('error.login.ldapname_not_found', ldapname: ldapname) + flash[:alert] = t('error.login.ldapname_not_found', ldapname:) redirect_to new_employee_session_path end end diff --git a/app/helpers/order_controlling_helper.rb b/app/helpers/order_controlling_helper.rb index 7fa6cc724..114af1e03 100644 --- a/app/helpers/order_controlling_helper.rb +++ b/app/helpers/order_controlling_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/helpers/order_helper.rb b/app/helpers/order_helper.rb index 697d49123..44b8f4eb6 100644 --- a/app/helpers/order_helper.rb +++ b/app/helpers/order_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -36,7 +38,7 @@ def order_target_icon(target) order_target_rating_icon( target.rating, - title: target.comment? ? simple_format(target.comment).gsub(/"/, '"') : nil, + title: target.comment? ? simple_format(target.comment).gsub('"', '"') : nil, data: { toggle: :tooltip } ) end @@ -70,7 +72,7 @@ def format_order_average_rate(value) end def format_order_additional_crm_orders(order) - simple_list (order.additional_crm_orders.map(&method(:crm_order_link))) + simple_list(order.additional_crm_orders.map(&method(:crm_order_link))) end def format_major_chance(order) @@ -92,7 +94,7 @@ def format_major_risk(order) end def glyphicons - %w(asterisk plus euro minus cloud envelope pencil glass music search heart star star-empty + %w[asterisk plus euro minus cloud envelope pencil glass music search heart star star-empty user film th-large th th-list ok remove zoom-in zoom-out off signal cog trash home file time road download-alt download upload inbox play-circle repeat refresh list-alt lock flag headphones volume-off volume-down volume-up qrcode barcode tag tags book bookmark print @@ -113,7 +115,7 @@ def glyphicons floppy-remove floppy-save floppy-open credit-card transfer cutlery header compressed earphone phone-alt tower stats sd-video hd-video subtitles sound-stereo sound-dolby sound-5-1 sound-6-1 sound-7-1 copyright-mark registration-mark cloud-download cloud-upload - tree-conifer tree-deciduous) + tree-conifer tree-deciduous] end def choosable_order_options @@ -122,16 +124,16 @@ def choosable_order_options end def order_option(order, selected = false) - if order - json = { id: order.id, - name: order.name, - path_shortnames: order.path_shortnames } - content_tag(:option, - order.label_verbose, - value: order.id, - selected: selected, - data: { data: json.to_json }) - end + return unless order + + json = { id: order.id, + name: order.name, + path_shortnames: order.path_shortnames } + content_tag(:option, + order.label_verbose, + value: order.id, + selected:, + data: { data: json.to_json }) end def order_progress_bar(order) @@ -139,7 +141,7 @@ def order_progress_bar(order) order_progress_bar_link(order.order, progress) do ''.html_safe.tap do |content| - if progress[:percent] > 0 + if (progress[:percent]).positive? content << content_tag( :div, nil, @@ -148,7 +150,7 @@ def order_progress_bar(order) ) end - if progress[:over_budget_percent] > 0 + if (progress[:over_budget_percent]).positive? content << content_tag( :div, nil, @@ -162,21 +164,21 @@ def order_progress_bar(order) private - def order_progress_bar_link(order, progress, &block) + def order_progress_bar_link(order, progress, &) title = "#{f(progress[:percent_title])}% geleistet" if can?(:show, order) link_to(order_order_controlling_url(order.id), - { class: 'progress', title: title }, - &block) + { class: 'progress', title: }, + &) else - content_tag(:div, yield, class: 'progress', title: title) + content_tag(:div, yield, class: 'progress', title:) end end def order_progress(order) progress = order_progress_hash - return progress unless order.offered_amount > 0 + return progress unless order.offered_amount.positive? calculate_order_progress(order, progress) progress @@ -195,13 +197,13 @@ def calculate_order_progress(order, progress) order.supplied_amount.to_f progress[:percent_title] = progress[:percent] - if order.supplied_amount.to_f > order.offered_amount.to_f - progress[:over_budget_percent] = - (order.supplied_amount.to_f - order.offered_amount.to_f) / - order.supplied_amount.to_f * - 100 - progress[:percent] = 100 - progress[:over_budget_percent] - end + return unless order.supplied_amount.to_f > order.offered_amount.to_f + + progress[:over_budget_percent] = + (order.supplied_amount.to_f - order.offered_amount.to_f) / + order.supplied_amount.to_f * + 100 + progress[:percent] = 100 - progress[:over_budget_percent] end def order_report_billability_class(value) @@ -230,8 +232,8 @@ def order_report_average_rate_class(value) def uncertainties_tooltip(order, uncertainty_type) uncertainties = uncertainties_grouped_by_risk(order, uncertainty_type) - [:high, :medium, :low] - .select { |risk| uncertainties.keys.include?(risk) } + %i[high medium low] + .select { |risk| uncertainties.key?(risk) } .reduce('') do |result, risk| title = t("activerecord.attributes.order_uncertainty/risks.#{risk}") names = uncertainties[risk].map { |u| "
  • #{h(u.name)}
  • " }.join diff --git a/app/helpers/order_services_helper.rb b/app/helpers/order_services_helper.rb index 94cda00d7..74d7dd2bc 100644 --- a/app/helpers/order_services_helper.rb +++ b/app/helpers/order_services_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -35,20 +37,18 @@ def checkable_worktimes_table(entries, options) def checkable_worktimes_data data = {} - if can?(:update, @order) - data[:row_link] = edit_ordertime_path(':id', back_url: url_for(returning: true)) - end + data[:row_link] = edit_ordertime_path(':id', back_url: url_for(returning: true)) if can?(:update, @order) data end def checkable_worktimes_footer(entries) - if entries.present? - footer = summed_worktimes_row(entries) - if entries.size == OrderServicesController::MAX_ENTRIES - too_many_entries_row - else - footer - end + return if entries.blank? + + footer = summed_worktimes_row(entries) + if entries.size == OrderServicesController::MAX_ENTRIES + too_many_entries_row + else + footer end end @@ -56,9 +56,7 @@ def worktimes_checkbox_column(t, options) check_all = check_box_tag(:all_worktimes, true, false, data: { check: 'worktime_ids[]' }) t.col(check_all, class: 'no-link') do |e| required_perm = options[:checkbox_requires_permission] - if required_perm.nil? || can?(required_perm, e) - check_box_tag('worktime_ids[]', e.id) - end + check_box_tag('worktime_ids[]', e.id) if required_perm.nil? || can?(required_perm, e) end end diff --git a/app/helpers/order_uncertainty_helper.rb b/app/helpers/order_uncertainty_helper.rb index 2629cd041..e0c87fd94 100644 --- a/app/helpers/order_uncertainty_helper.rb +++ b/app/helpers/order_uncertainty_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/helpers/periods_helper.rb b/app/helpers/periods_helper.rb index c1ac909c3..5ce72d13a 100644 --- a/app/helpers/periods_helper.rb +++ b/app/helpers/periods_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module PeriodsHelper def period_link(label, shortcut, options = {}) link_to(label, diff --git a/app/helpers/plannings_helper.rb b/app/helpers/plannings_helper.rb index ad6ba86f8..e567581e5 100644 --- a/app/helpers/plannings_helper.rb +++ b/app/helpers/plannings_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/helpers/reports_workload_helper.rb b/app/helpers/reports_workload_helper.rb index 088c8901d..7f227eb30 100644 --- a/app/helpers/reports_workload_helper.rb +++ b/app/helpers/reports_workload_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -42,9 +44,9 @@ def workload_absences_url(employee_id, report) def workload_worktime_balance_class(value) config = Settings.reports.workload.worktime_balance - if value < config.lower_limit - 'red' - end + return unless value < config.lower_limit + + 'red' end def workload_worktime_billability_class(value) diff --git a/app/helpers/sort_helper.rb b/app/helpers/sort_helper.rb index 1b462f3b9..74150728d 100644 --- a/app/helpers/sort_helper.rb +++ b/app/helpers/sort_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/helpers/table_helper.rb b/app/helpers/table_helper.rb index 00632b4cb..9dc33e2ea 100644 --- a/app/helpers/table_helper.rb +++ b/app/helpers/table_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -17,7 +19,7 @@ module TableHelper # appended to the attribute columns. # If entries is empty, an appropriate message is rendered. # An options hash may be given as the last argument. - def plain_table(entries, *attrs, &_block) + def plain_table(entries, *attrs) options = attrs.extract_options! add_css_class(options, 'table table-striped table-hover table-condensed') builder = options.delete(:builder) || DryCrud::Table::Builder @@ -41,22 +43,22 @@ def plain_table_or_message(entries, *attrs, &block) end # Same as plain_table_or_message, but wraps table in .unindented - def unindented_plain_table_or_message(entries, *attrs, &block) + def unindented_plain_table_or_message(entries, *attrs, &) entries.to_a # force evaluation of relations if entries.present? content_tag(:div, class: 'unindented') do - plain_table_or_message(entries, *attrs, &block) + plain_table_or_message(entries, *attrs, &) end else - plain_table_or_message(entries, *attrs, &block) + plain_table_or_message(entries, *attrs, &) end end # Create a table of the +entries+ with the default or # the passed attributes in its columns. An options hash may be given # as the last argument. - def list_table(*attrs, &block) - attrs, options = explode_attrs_with_options(attrs, &block) + def list_table(*attrs, &) + attrs, options = explode_attrs_with_options(attrs, &) plain_table_or_message(entries, options) do |t| t.sortable_attrs(*attrs) yield t if block_given? @@ -70,8 +72,8 @@ def list_table(*attrs, &block) # If a block is given, the column defined there will be inserted # between the given attributes and the actions. # An options hash for the table builder may be given as the last argument. - def crud_table(*attrs, &block) - attrs, options = explode_attrs_with_options(attrs, &block) + def crud_table(*attrs, &) + attrs, options = explode_attrs_with_options(attrs, &) first = attrs.shift plain_table_or_message(entries, options) do |t| t.attr_with_show_link(first) if first @@ -89,11 +91,9 @@ def standard_table_actions(table) private - def explode_attrs_with_options(attrs, &_block) + def explode_attrs_with_options(attrs, &) options = attrs.extract_options! - if !block_given? && attrs.blank? - attrs = default_crud_attrs - end + attrs = default_crud_attrs if !block_given? && attrs.blank? [attrs, options] end end diff --git a/app/helpers/tooltip_helper.rb b/app/helpers/tooltip_helper.rb index 567d87422..502283a3b 100644 --- a/app/helpers/tooltip_helper.rb +++ b/app/helpers/tooltip_helper.rb @@ -1,14 +1,14 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. module TooltipHelper - def with_tooltip(tooltip_text, options = {}) + def with_tooltip(tooltip_text, options = {}, &) tag = options.delete(:tag) || :span options = options.merge(title: tooltip_text, data: { toggle: :tooltip }) - content_tag(tag, options) do - yield - end + content_tag(tag, options, &) end end diff --git a/app/helpers/utility_helper.rb b/app/helpers/utility_helper.rb index 588081e76..43aba1702 100644 --- a/app/helpers/utility_helper.rb +++ b/app/helpers/utility_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -9,8 +11,8 @@ module UtilityHelper # Render a content tag with the collected contents rendered # by &block for each item in collection. - def content_tag_nested(tag, collection, options = {}, &block) - content_tag(tag, safe_join(collection, &block), options) + def content_tag_nested(tag, collection, options = {}, &) + content_tag(tag, safe_join(collection, &), options) end def icon(icon, options = {}) @@ -31,8 +33,8 @@ def currency # Overridden method that takes a block that is executed for each item in # array before appending the results. - def safe_join(array, sep = $OUTPUT_FIELD_SEPARATOR, &block) - super(block_given? ? array.map(&block).compact : array, sep) + def safe_join(array, sep = $OUTPUT_FIELD_SEPARATOR, &) + super(block_given? ? array.map(&).compact : array, sep) end # Returns the css class for the given flash level. @@ -47,7 +49,7 @@ def flash_class(level) # Adds a class to the given options, even if there are already classes. def add_css_class(options, classes) if options[:class] - options[:class] += ' ' + classes if classes + options[:class] += " #{classes}" if classes else options[:class] = classes end @@ -58,7 +60,7 @@ def add_css_class(options, classes) # 'id' or 'position'. def default_crud_attrs attrs = model_class.column_names.map(&:to_sym) - attrs - [:id, :position, :password] + attrs - %i[id position password] end # Returns the ActiveRecord column type or nil. @@ -68,9 +70,9 @@ def column_type(obj, attr) # Returns an ActiveRecord column property for the passed attr or nil def column_property(obj, attr, property) - if obj.respond_to?(:column_for_attribute) && obj.has_attribute?(attr) - obj.column_for_attribute(attr).send(property) - end + return unless obj.respond_to?(:column_for_attribute) && obj.has_attribute?(attr) + + obj.column_for_attribute(attr).send(property) end # Returns the association proxy for the given attribute. The attr parameter @@ -79,11 +81,11 @@ def column_property(obj, attr, property) # any association is returned. Returns nil if no association (or not of the # given macro) was found. def association(obj, attr, *macros) - if obj.class.respond_to?(:reflect_on_association) - name = assoc_and_id_attr(attr).first.to_sym - assoc = obj.class.reflect_on_association(name) - assoc if assoc && (macros.blank? || macros.include?(assoc.macro)) - end + return unless obj.class.respond_to?(:reflect_on_association) + + name = assoc_and_id_attr(attr).first.to_sym + assoc = obj.class.reflect_on_association(name) + assoc if assoc && (macros.blank? || macros.include?(assoc.macro)) end # Returns the name of the attr and it's corresponding field diff --git a/app/helpers/worktime_helper.rb b/app/helpers/worktime_helper.rb index ea5bf9a05..b9d8ed4b0 100644 --- a/app/helpers/worktime_helper.rb +++ b/app/helpers/worktime_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -5,7 +7,7 @@ module WorktimeHelper def worktime_account(worktime) - worktime.account.label_verbose if worktime.account + worktime.account&.label_verbose end def worktime_description(worktime) @@ -13,17 +15,17 @@ def worktime_description(worktime) end def work_item_option(item) - if item - json = { id: item.id, - name: item.name, - path_shortnames: item.path_shortnames, - description: item.description } - content_tag(:option, - item.label_verbose, - value: item.id, - selected: true, - data: { data: json.to_json }) - end + return unless item + + json = { id: item.id, + name: item.name, + path_shortnames: item.path_shortnames, + description: item.description } + content_tag(:option, + item.label_verbose, + value: item.id, + selected: true, + data: { data: json.to_json }) end def overview_day_class(_worktimes, day) @@ -40,9 +42,7 @@ def time_range(worktime) result = ' ' if worktime.from_start_time.present? result = "#{format_time(worktime.from_start_time)} - " - if worktime.to_end_time.present? - result += format_time(worktime.to_end_time) - end + result += format_time(worktime.to_end_time) if worktime.to_end_time.present? end result.html_safe end @@ -63,9 +63,9 @@ def monthly_worktime # sum worktime hours for a given date. if no date is given, sum all worktime hours def sum_hours(day = nil) if day - @daily_worktimes[day] ? @daily_worktimes[day].map(&:hours).sum : 0 + @daily_worktimes[day] ? @daily_worktimes[day].sum(&:hours) : 0 else - @worktimes.map(&:hours).sum + @worktimes.sum(&:hours) end end end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index fa06d2510..87519b74d 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2019, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/jobs/commit_reminder_job.rb b/app/jobs/commit_reminder_job.rb new file mode 100644 index 000000000..a3779fa3f --- /dev/null +++ b/app/jobs/commit_reminder_job.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Copyright (c) 2006-2022, Puzzle ITC GmbH. This file is part of +# PuzzleTime and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/puzzle/puzzletime. + +class CommitReminderJob < CronJob + self.cron_expression = '0 5 2 * *' + + def perform + Employee.active_employed_last_month.pending_worktimes_commit.where(worktimes_commit_reminder: true).find_each do |employee| + EmployeeMailer.worktime_commit_reminder_mail(employee).deliver_now + end + end +end diff --git a/app/jobs/crm_sync_job.rb b/app/jobs/crm_sync_job.rb index 70494e293..e8132881c 100644 --- a/app/jobs/crm_sync_job.rb +++ b/app/jobs/crm_sync_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/jobs/cron_job.rb b/app/jobs/cron_job.rb index a2a559213..7df6320c7 100644 --- a/app/jobs/cron_job.rb +++ b/app/jobs/cron_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2019, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/jobs/invoicing_sync_job.rb b/app/jobs/invoicing_sync_job.rb index 8817028a1..b97a00b16 100644 --- a/app/jobs/invoicing_sync_job.rb +++ b/app/jobs/invoicing_sync_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 000000000..6338951f3 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ApplicationMailer < ActionMailer::Base + layout 'mailer' + helper FormatHelper + helper EmailHelper + + default from: Settings.mailer.from +end diff --git a/app/mailers/employee_mailer.rb b/app/mailers/employee_mailer.rb index 88bdecc50..a58d0832e 100644 --- a/app/mailers/employee_mailer.rb +++ b/app/mailers/employee_mailer.rb @@ -1,12 +1,20 @@ -class EmployeeMailer < ActionMailer::Base - add_template_helper FormatHelper +# frozen_string_literal: true +class EmployeeMailer < ApplicationMailer def worktime_deleted_mail(worktime, deleted_by) @worktime = worktime @deleted_by = deleted_by - mail(from: Settings.mailer.employee.worktime_deleted.from, - to: worktime.employee.email, + mail(to: worktime.employee.email, subject: 'PuzzleTime-Eintrag wurde gelöscht') end + + def worktime_commit_reminder_mail(employee) + @employee = employee + + mail( + to: "#{employee.firstname} #{employee.lastname} <#{employee.email}>", + subject: 'PuzzleTime Zeiten freigeben' + ) + end end diff --git a/app/models/ability.rb b/app/models/ability.rb index d9e014705..3b024bef1 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -54,60 +56,61 @@ def management_abilities EmploymentRoleLevel, EmploymentRoleCategory, Reports::Workload, - WorkItem] + WorkItem, + Workplace] # :crud instead of :manage because cannot change settings of other employees - can [:crud, - :update_committed_worktimes, - :update_reviewed_worktimes, - :manage_plannings, - :show_worktime_graph, - :social_insurance, - :log], + can %i[crud + update_committed_worktimes + update_reviewed_worktimes + manage_plannings + show_worktime_graph + social_insurance + log], Employee can :update, OrderComment do |c| c.creator_id == user.id end - can [:create, :read], OrderComment + can %i[create read], OrderComment # cannot change settings of other employees can [:crud, :update_committed_worktimes], Employee do |_| true end - can [:create, - :read, - :sync, - :preview_total, - :billing_addresses, - :filter_fields], + can %i[create + read + sync + preview_total + billing_addresses + filter_fields], Invoice can [:edit, :update, :destroy], Invoice do |i| - !%w(deleted paid partially_paid).include?(i.status) + %w[deleted paid partially_paid].exclude?(i.status) end can [:read], Worktime - can [:create, :destroy], Absencetime + can %i[create destroy], Absencetime can [:create, :update, :destroy], Ordertime do |t| !t.work_item_closed? && !t.invoice_sent_or_paid? end - can [:clients, - :employees, - :overtime, - :clientworkitems, - :departments, - :departmentorders, - :managed, - :employeeworkitems, - :employeesubworkitems, - :absences, - :employeeabsences, - :capacity_report, - :export_report, - :meal_compensation], - Evaluation + can %i[clients + employees + overtime + clientworkitems + departments + departmentorders + managed + employeeworkitems + employeesubworkitems + absences + employeeabsences + capacity_report + export_report + meal_compensation], + Evaluations::Evaluation end def order_responsible_abilities @@ -120,7 +123,7 @@ def order_responsible_abilities can :update, OrderComment do |c| c.creator_id == user.id end - can [:create, :read], OrderComment + can %i[create read], OrderComment can :manage, [AccountingPost, Contract, OrderUncertainty] do |instance| instance.order.responsible_id == user.id @@ -137,8 +140,8 @@ def order_responsible_abilities end can [:edit, :update, :destroy], Invoice do |i| is_responsible = (i.order.responsible_id == user.id) - is_open = !%w(deleted paid partially_paid).include?(i.status) - is_manual_and_used = (i.manual? && i.total_amount > 1) + is_open = %w[deleted paid partially_paid].exclude?(i.status) + is_manual_and_used = i.manual? && i.total_amount > 1 is_responsible && is_open && !is_manual_and_used end @@ -149,7 +152,7 @@ def order_responsible_abilities can [:create, :update, :destroy], Ordertime do |t| t.order.responsible_id == user.id && !t.work_item_closed? && !t.invoice_sent_or_paid? end - can :managed, Evaluation + can :managed, Evaluations::Evaluation can :manage, Planning do |planning| planning.order.responsible_id == user.id @@ -159,15 +162,14 @@ def order_responsible_abilities can :social_insurance, Employee end - def api_client_abilities - end + def api_client_abilities; end def everyone_abilities - can [:read, - :existing, - :split, - :create_part, - :delete_part], + can %i[read + existing + split + create_part + delete_part], Worktime, employee_id: user.id @@ -182,33 +184,31 @@ def everyone_abilities can :search, WorkItem - can [:read, :show_plannings], Employee - can [:passwd, - :update_passwd, - :settings, - :update_settings, - :update_committed_worktimes, - :show_worktime_graph, - :social_insurance, - :manage_plannings], + can %i[read show_plannings], Employee + can %i[settings + update_settings + update_committed_worktimes + show_worktime_graph + social_insurance + manage_plannings], Employee, id: user.id can :index, Employment, employee_id: user.id - can [:read, - :accounting_posts, - :controlling, - :create_comment, - :search, - :services, - :show_targets, - :show_uncertainties, - :show_contract, - :show_comments, - :show_invoices, - :reports, - :show_plannings], + can %i[read + accounting_posts + controlling + create_comment + search + services + show_targets + show_uncertainties + show_contract + show_comments + show_invoices + reports + show_plannings], Order can :show_plannings, AccountingPost @@ -220,19 +220,19 @@ def everyone_abilities can :manage, CustomList, employee_id: user.id - can [:compose_report, - :report, - :export_csv, - :userworkitems, - :userabsences, - :usersubworkitems, - :subworkitems, - :workitememployees, - :orderworkitems], - Evaluation + can %i[compose_report + report + export_csv + userworkitems + userabsences + usersubworkitems + subworkitems + workitememployees + orderworkitems], + Evaluations::Evaluation can :manage, Expense, employee_id: user.id - can [:create, :read], OrderComment + can %i[create read], OrderComment end end diff --git a/app/models/absence.rb b/app/models/absence.rb index 9c471ad8a..92445f776 100644 --- a/app/models/absence.rb +++ b/app/models/absence.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -13,7 +15,7 @@ # vacation :boolean default(FALSE), not null # -class Absence < ActiveRecord::Base +class Absence < ApplicationRecord include Evaluatable # All dependencies between the models are listed below diff --git a/app/models/absencetime.rb b/app/models/absencetime.rb index fea317f8f..b57b85e77 100644 --- a/app/models/absencetime.rb +++ b/app/models/absencetime.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/models/accounting_post.rb b/app/models/accounting_post.rb index 1b3010ef9..aff2f9037 100644 --- a/app/models/accounting_post.rb +++ b/app/models/accounting_post.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -22,7 +24,7 @@ # service_id :integer # -class AccountingPost < ActiveRecord::Base +class AccountingPost < ApplicationRecord include BelongingToWorkItem include Closable @@ -37,8 +39,8 @@ class AccountingPost < ActiveRecord::Base ### CALLBACKS before_validation :derive_offered_fields - before_update :remember_old_work_item_id after_create :move_order_accounting_post_work_item + before_update :remember_old_work_item_id after_update :handle_changed_work_item ### VALIDATIONS @@ -51,20 +53,18 @@ class AccountingPost < ActiveRecord::Base ### INSTANCE METHODS def validate_worktime(worktime) - return if worktime.report_type == AutoStartType::INSTANCE + return if worktime.report_type == ReportType::AutoStartType::INSTANCE if description_required? && worktime.description.blank? worktime.errors.add(:description, 'Es muss eine Bemerkung angegeben werden') end - if ticket_required? && worktime.ticket.blank? - worktime.errors.add(:ticket, 'Es muss ein Ticket angegeben werden') - end + worktime.errors.add(:ticket, 'Es muss ein Ticket angegeben werden') if ticket_required? && worktime.ticket.blank? - if from_to_times_required? - worktime.errors.add(:from_start_time, 'muss angegeben werden') if worktime.from_start_time.blank? - worktime.errors.add(:to_end_time, 'muss angegeben werden') if worktime.to_end_time.blank? - end + return unless from_to_times_required? + + worktime.errors.add(:from_start_time, 'muss angegeben werden') if worktime.from_start_time.blank? + worktime.errors.add(:to_end_time, 'muss angegeben werden') if worktime.to_end_time.blank? end def attach_work_item(order, attributes, book_on_order = false) @@ -113,10 +113,10 @@ def derive_offered_fields end def check_booked_on_order - if booked_on_order? && !book_on_order_allowed? - errors.add(:base, "'Direkt auf Auftrag buchen' gewählt, aber es existieren bereits andere Buchungspositionen") - false - end + return unless booked_on_order? && !book_on_order_allowed? + + errors.add(:base, "'Direkt auf Auftrag buchen' gewählt, aber es existieren bereits andere Buchungspositionen") + false end def remember_old_work_item_id @@ -136,22 +136,22 @@ def move_order_accounting_post_work_item return if work_item_id == order.work_item_id post = order.accounting_posts.find_by(work_item_id: order.work_item_id) - if post - begin - post.work_item = WorkItem.create(name: order.work_item.name, - shortname: order.work_item.shortname, - parent_id: order.work_item.id, - closed: post.closed? || order.status.closed?) - post.save! - rescue ActiveRecord::RecordInvalid => error - validation_messages = post.errors.full_messages.join(', ') - msg = "Bestehende Buchungsposition ist ungültig und muss zuerst korrigiert werden: #{validation_messages}" - errors.add(:base, msg) - throw(:abort) - end - order.work_item.move_times!(post.work_item) - order.work_item.move_plannings!(post.work_item) + return unless post + + begin + post.work_item = WorkItem.create(name: order.work_item.name, + shortname: order.work_item.shortname, + parent_id: order.work_item.id, + closed: post.closed? || order.status.closed?) + post.save! + rescue ActiveRecord::RecordInvalid + validation_messages = post.errors.full_messages.join(', ') + msg = "Bestehende Buchungsposition ist ungültig und muss zuerst korrigiert werden: #{validation_messages}" + errors.add(:base, msg) + throw(:abort) end + order.work_item.move_times!(post.work_item) + order.work_item.move_plannings!(post.work_item) end def exclusive_work_item? diff --git a/app/models/additional_crm_order.rb b/app/models/additional_crm_order.rb index 871386794..6b079a480 100644 --- a/app/models/additional_crm_order.rb +++ b/app/models/additional_crm_order.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -13,7 +15,7 @@ # name :string # -class AdditionalCrmOrder < ActiveRecord::Base +class AdditionalCrmOrder < ApplicationRecord belongs_to :order validates_by_schema @@ -29,8 +31,8 @@ def to_s private def sync_name - if Crm.instance - Crm.instance.delay.sync_additional_order(self) - end + return unless Crm.instance + + Crm.instance.delay.sync_additional_order(self) end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 000000000..10a4cba84 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/authentication.rb b/app/models/authentication.rb index e13ddb4d7..e678c07cd 100644 --- a/app/models/authentication.rb +++ b/app/models/authentication.rb @@ -1,3 +1,5 @@ -class Authentication < ActiveRecord::Base +# frozen_string_literal: true + +class Authentication < ApplicationRecord belongs_to :employee end diff --git a/app/models/billing_address.rb b/app/models/billing_address.rb index 9e53ef478..7f96e7030 100644 --- a/app/models/billing_address.rb +++ b/app/models/billing_address.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -18,7 +20,7 @@ # invoicing_key :string # -class BillingAddress < ActiveRecord::Base +class BillingAddress < ApplicationRecord protect_if :invoices, 'Dieser Eintrag kann nicht gelöscht werden, da ihm noch Rechnungen zugeordnet sind' @@ -29,7 +31,7 @@ class BillingAddress < ActiveRecord::Base has_many :invoices validates_by_schema - validates :client_id, :street, :zip_code, :town, :country, presence: true + validates :street, :zip_code, :town, :country, presence: true validates :invoicing_key, uniqueness: true, allow_blank: true validates :country, inclusion: ISO3166::Data.codes validate :assert_contact_belongs_to_client @@ -54,9 +56,9 @@ def country_name private def assert_contact_belongs_to_client - if contact_id && client_id && contact.client_id != client_id - errors.add(:contact_id, 'muss zum gleichen Kunden gehören.') - end + return unless contact_id && client_id && contact.client_id != client_id + + errors.add(:contact_id, 'muss zum gleichen Kunden gehören.') end def set_default_country diff --git a/app/models/client.rb b/app/models/client.rb index 53d725da3..c652b5985 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -17,7 +19,7 @@ # e_bill_account_key :string # -class Client < ActiveRecord::Base +class Client < ApplicationRecord include BelongingToWorkItem include Evaluatable diff --git a/app/models/company.rb b/app/models/company.rb index 17a7a8040..87087a0b9 100644 --- a/app/models/company.rb +++ b/app/models/company.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -6,16 +8,12 @@ # Helper class to provide information for the organisation represented by PuzzleTime. class Company class << self - def name - client.name - end + delegate :name, to: :client def client RequestStore.store['company_client'] ||= Client.find(Settings.clients.company_id) end - def work_item_id - client.work_item_id - end + delegate :work_item_id, to: :client end end diff --git a/app/models/concerns/belonging_to_work_item.rb b/app/models/concerns/belonging_to_work_item.rb index 6b209cb8d..5cdbf1e9d 100644 --- a/app/models/concerns/belonging_to_work_item.rb +++ b/app/models/concerns/belonging_to_work_item.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -17,8 +19,6 @@ module BelongingToWorkItem after_destroy :destroy_exclusive_work_item - validates :work_item, presence: true - accepts_nested_attributes_for :work_item, update_only: true delegate :name, :shortname, :description, :path_names, :path_shortnames, :label_verbose, @@ -26,23 +26,23 @@ module BelongingToWorkItem allow_nil: true scope :list, lambda { - includes(:work_item). - references(:work_item). - order('work_items.path_names') + includes(:work_item) + .references(:work_item) + .order('work_items.path_names') } end def to_s - work_item.to_s if work_item + work_item&.to_s end private def destroy_exclusive_work_item - if !@item_destroying && exclusive_work_item? - @item_destroying = true - work_item.destroy - end + return unless !@item_destroying && exclusive_work_item? + + @item_destroying = true + work_item.destroy end def exclusive_work_item? @@ -56,16 +56,16 @@ def has_ancestor_through_work_item(name) work_item.with_ancestors.detect { |a| a.send(name) }.try(name) else model.joins('LEFT JOIN work_items ON ' \ - "#{model.table_name}.work_item_id = ANY (work_items.path_ids)"). - find_by('work_items.id = ?', work_item_id) + "#{model.table_name}.work_item_id = ANY (work_items.path_ids)") + .find_by('work_items.id = ?', work_item_id) end end end def has_descendants_through_work_item(name) memoized_method(name) do |model| - model.joins(:work_item). - where('? = ANY (work_items.path_ids)', work_item_id) + model.joins(:work_item) + .where('? = ANY (work_items.path_ids)', work_item_id) end end @@ -74,8 +74,8 @@ def has_descendants_through_work_item(name) def memoized_method(name, &block) model = name.to_s.classify.constantize define_method(name) do - instance_variable_get("@#{name}") || - instance_variable_set("@#{name}", instance_exec(model, &block)) + instance_variable_get(:"@#{name}") || + instance_variable_set(:"@#{name}", instance_exec(model, &block)) end end end diff --git a/app/models/concerns/closable.rb b/app/models/concerns/closable.rb index b84a0659e..efa327f9c 100644 --- a/app/models/concerns/closable.rb +++ b/app/models/concerns/closable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -23,15 +25,13 @@ def open? private def remember_closed_change - if closed_changed? - @closed_changed = true - end + return unless closed_changed? + + @closed_changed = true end def propagate_closed_change - if @closed_changed - propagate_closed! - end + propagate_closed! if @closed_changed @closed_changed = nil end end diff --git a/app/models/concerns/conditioner.rb b/app/models/concerns/conditioner.rb index 0911c68d8..ee5e322bf 100644 --- a/app/models/concerns/conditioner.rb +++ b/app/models/concerns/conditioner.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -13,7 +15,7 @@ def append_conditions(existing, appends, cat = 'AND') existing[0] = "( #{existing[0]} ) #{cat} " end existing[0] += appends[0] - existing.concat appends[1..-1] + existing.concat appends[1..] end def clone_conditions(conditions) diff --git a/app/models/concerns/evaluatable.rb b/app/models/concerns/evaluatable.rb index 0026ebcd1..3b1c0368b 100644 --- a/app/models/concerns/evaluatable.rb +++ b/app/models/concerns/evaluatable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/models/contact.rb b/app/models/contact.rb index f942a7ad2..58296e52c 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -21,8 +23,8 @@ # invoicing_key :string # -class Contact < ActiveRecord::Base - CRM_ID_PREFIX = 'crm_'.freeze +class Contact < ApplicationRecord + CRM_ID_PREFIX = 'crm_' belongs_to :client @@ -31,7 +33,8 @@ class Contact < ActiveRecord::Base has_many :billing_addresses, dependent: :nullify validates_by_schema - validates :firstname, :lastname, :client_id, presence: true + validates :firstname, :lastname, presence: true + validates :email, email: true, allow_blank: true validates :invoicing_key, uniqueness: true, allow_blank: true scope :list, -> { order(:lastname, :firstname) } diff --git a/app/models/contract.rb b/app/models/contract.rb index 396528a60..da68f4ece 100644 --- a/app/models/contract.rb +++ b/app/models/contract.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -17,7 +19,7 @@ # notes :text # -class Contract < ActiveRecord::Base +class Contract < ApplicationRecord has_one :order validates_by_schema diff --git a/app/models/custom_list.rb b/app/models/custom_list.rb index 3d4439b26..6820cb577 100644 --- a/app/models/custom_list.rb +++ b/app/models/custom_list.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -14,7 +16,7 @@ # item_ids :integer not null, is an Array # -class CustomList < ActiveRecord::Base +class CustomList < ApplicationRecord belongs_to :employee, optional: true validates_by_schema except: :item_ids diff --git a/app/models/department.rb b/app/models/department.rb index 6ea905268..7903fe3c3 100644 --- a/app/models/department.rb +++ b/app/models/department.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -11,7 +13,7 @@ # name :string(255) not null # shortname :string(3) not null # -class Department < ActiveRecord::Base +class Department < ApplicationRecord include Evaluatable has_many :orders @@ -31,17 +33,17 @@ def to_s end def worktimes - Worktime. - joins(:work_item). - joins('INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)'). - where(orders: { department_id: id }) + Worktime + .joins(:work_item) + .joins('INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)') + .where(orders: { department_id: id }) end def plannings - Planning. - joins(:work_item). - joins('INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)'). - where(orders: { department_id: id }) + Planning + .joins(:work_item) + .joins('INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)') + .where(orders: { department_id: id }) end ##### interface methods for Evaluatable ##### diff --git a/app/models/employee.rb b/app/models/employee.rb index 0ac6dab77..09fe61ecc 100644 --- a/app/models/employee.rb +++ b/app/models/employee.rb @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# frozen_string_literal: true # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 @@ -40,7 +40,7 @@ # identity_card_valid_until :date # -class Employee < ActiveRecord::Base +class Employee < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, @@ -51,7 +51,7 @@ class Employee < ActiveRecord::Base # :registerable, # :recoverable, - INTERNAL_ATTRS = %w(id passwd eval_periods encrypted_password updated_at created_at).freeze + INTERNAL_ATTRS = %w[id eval_periods encrypted_password updated_at created_at].freeze include Evaluatable include ReportType::Accessors @@ -59,16 +59,17 @@ class Employee < ActiveRecord::Base has_paper_trail(meta: { employee_id: proc(&:id) }, skip: Employee::INTERNAL_ATTRS) - enum marital_status: %w( + enum marital_status: %w[ single married widowed civil_partnership divorced - ).freeze + ].freeze # All dependencies between the models are listed below. belongs_to :department, optional: true + belongs_to :workplace, optional: true has_and_belongs_to_many :invoices @@ -85,13 +86,13 @@ class Employee < ActiveRecord::Base has_many :custom_lists, dependent: :destroy has_many :plannings, dependent: :destroy has_one :running_time, - -> { where(report_type: AutoStartType::INSTANCE.key) }, + -> { where(report_type: ReportType::AutoStartType::INSTANCE.key) }, class_name: 'Ordertime' has_many :expenses, dependent: :destroy has_many :authentications, dependent: :destroy before_validation do - self.nationalities.try(:reject!, &:blank?) + nationalities.try(:reject!, &:blank?) end # Validation helpers. @@ -105,6 +106,12 @@ class Employee < ActiveRecord::Base scope :list, -> { order('lastname', 'firstname') } scope :current, -> { joins(:employments).merge(Employment.during(Period.current_day)) } + # logic should match CompletableHelper#recently_completed + scope :pending_worktimes_commit, lambda { + where("committed_worktimes_at < date_trunc('month', now()) - '1 day'::interval").or(where(committed_worktimes_at: nil)) + } + scope :active_employed_last_month, -> { joins(:employments).merge(Employment.active.during(Period.previous_month)) } + # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, @@ -135,18 +142,11 @@ def providers end class << self - # Tries to login a user with the passed data. - # Returns the logged-in Employee or nil if the login failed. - def login(username, pwd) - find_by(shortname: username.upcase, passwd: encode(pwd)) || - LdapAuthenticator.new.login(username, pwd) - end - def employed_ones(period, sort = true) - result = joins('left join employments em on em.employee_id = employees.id'). - where('(em.end_date IS null or em.end_date >= ?) AND em.start_date <= ?', - period.start_date, period.end_date). - distinct + result = joins('left join employments em on em.employee_id = employees.id') + .where('(em.end_date IS null or em.end_date >= ?) AND em.start_date <= ?', + period.start_date, period.end_date) + .distinct sort ? result.list : result end @@ -159,10 +159,10 @@ def encode(pwd) end def with_worktimes_in_period(order, from, to) - e_ids = order.worktimes. - in_period(Period.new(from, to)). - billable. - select(:employee_id) + e_ids = order.worktimes + .in_period(Period.new(from, to)) + .billable + .select(:employee_id) Employee.where(id: e_ids) end end @@ -186,34 +186,22 @@ def eval_periods super || [] end - def before_create - self.passwd = '' # disable password login - end - - def check_passwd(pwd) - passwd == Employee.encode(pwd) - end - - def update_passwd!(pwd) - update_attributes!(passwd: Employee.encode(pwd)) - end - # main work items this employee ever worked on def alltime_main_work_items - WorkItem.select('DISTINCT work_items.*'). - joins('RIGHT JOIN work_items leaves ON leaves.path_ids[1] = work_items.id'). - joins('RIGHT JOIN worktimes ON worktimes.work_item_id = leaves.id'). - where(worktimes: { employee_id: id }). - where('work_items.id IS NOT NULL'). - list + WorkItem.select('DISTINCT work_items.*') + .joins('RIGHT JOIN work_items leaves ON leaves.path_ids[1] = work_items.id') + .joins('RIGHT JOIN worktimes ON worktimes.work_item_id = leaves.id') + .where(worktimes: { employee_id: id }) + .where.not(work_items: { id: nil }) + .list end def alltime_leaf_work_items - WorkItem.select('DISTINCT work_items.*'). - joins('RIGHT JOIN worktimes ON worktimes.work_item_id = work_items.id'). - where(worktimes: { employee_id: id }). - where('work_items.id IS NOT NULL'). - list + WorkItem.select('DISTINCT work_items.*') + .joins('RIGHT JOIN worktimes ON worktimes.work_item_id = work_items.id') + .where(worktimes: { employee_id: id }) + .where.not(work_items: { id: nil }) + .list end def statistics @@ -247,7 +235,7 @@ def current_percent # Returns the employment percent value for a given employment date def percent(date) empl = employment_at(date) - empl.percent if empl + empl&.percent end # Returns the employement at the given date, nil if none is present. @@ -271,9 +259,7 @@ def periods_format def validate_periods_format(attr, periods) periods.each do |p| - unless p =~ /^\-?\d[dwmqy]?$/ - errors.add(attr, 'ist nicht gültig') - end + errors.add(attr, 'ist nicht gültig') unless /^-?\d[dwmqy]?$/.match?(p) end end end diff --git a/app/models/employment.rb b/app/models/employment.rb index 5da9624cd..8e15b064e 100644 --- a/app/models/employment.rb +++ b/app/models/employment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -16,7 +18,7 @@ # comment :string # -class Employment < ActiveRecord::Base +class Employment < ApplicationRecord DAYS_PER_YEAR = 365.25 has_paper_trail(meta: { employee_id: proc(&:employee_id) }) @@ -33,7 +35,6 @@ class Employment < ActiveRecord::Base # All dependencies between the models are listed below. validates_by_schema validates :percent, inclusion: 0..200 - validates :employee_id, presence: true validates :vacation_days_per_year, numericality: { greater_or_equal_than: 0, less_than_or_equal_to: 366, allow_blank: true } validates :start_date, :end_date, timeliness: { date: true, allow_blank: true } @@ -43,37 +44,47 @@ class Employment < ActiveRecord::Base before_create :update_previous_end_date scope :list, -> { order('start_date DESC') } + scope :active, -> { where('"employments"."percent" > 0') } class << self def during(period) return all unless period - conditions = [''] - - if period.start_date - conditions.first << '("employments"."end_date" is NULL OR "employments"."end_date" >= ?)' - conditions << period.start_date - end - - if period.end_date - conditions.first << ' AND ' if conditions.first.present? - conditions.first << '"employments"."start_date" <= ?' - conditions << period.end_date - end + conditions = [ + condition_query(period), + period.start_date, + period.end_date + ].compact where(*conditions) end def normalize_boundaries(employments, period) employments.each do |e| - if period.start_date && e.start_date < period.start_date - e.start_date = period.start_date - end - if period.end_date && (e.end_date.nil? || e.end_date > period.end_date) - e.end_date = period.end_date - end + e.start_date = period.start_date if period.start_date && e.start_date < period.start_date + e.end_date = period.end_date if period.end_date && (e.end_date.nil? || e.end_date > period.end_date) end end + + private + + def condition_query(period) + [ start_condition(period), end_condition(period)] + .compact + .join(' AND ') + end + + def start_condition(period) + return unless period.start_date + + '("employments"."end_date" is NULL OR "employments"."end_date" >= ?)' + end + + def end_condition(period) + return unless period.end_date + + '"employments"."start_date" <= ?' + end end def previous_employment @@ -84,13 +95,13 @@ def previous_employment def following_employment @following_employment ||= - Employment.where('employee_id = ? AND start_date > ?', employee_id, start_date). - order('start_date'). - first + Employment.where('employee_id = ? AND start_date > ?', employee_id, start_date) + .order('start_date') + .first end def period - Period.new(start_date, end_date ? end_date : Time.zone.today) if start_date + Period.new(start_date, end_date || Time.zone.today) if start_date end def percent_factor @@ -131,9 +142,7 @@ def update_previous_end_date previous_employment.end_date = start_date - 1 previous_employment.save! end - if following_employment - self.end_date ||= following_employment.start_date - 1 - end + self.end_date ||= following_employment.start_date - 1 if following_employment true end @@ -153,14 +162,14 @@ def parallel_employments? end if end_date conditions[0] += ' AND (' \ - '(start_date <= ? AND (end_date >= ?' + (new_record? ? '' : ' OR end_date IS NULL') + ') ) OR ' \ - '(start_date <= ? AND (end_date >= ?' + (new_record? ? '' : ' OR end_date IS NULL') + ') ) OR ' \ - '(start_date >= ? AND end_date <= ? ))' + '(start_date <= ? AND (end_date >= ?' + (new_record? ? '' : ' OR end_date IS NULL') + ') ) OR ' \ + '(start_date <= ? AND (end_date >= ?' + (new_record? ? '' : ' OR end_date IS NULL') + ') ) OR ' \ + '(start_date >= ? AND end_date <= ? ))' conditions.push(start_date, start_date, end_date, end_date, start_date, end_date) else conditions[0] += ' AND (start_date = ? OR (start_date <= ? AND end_date >= ?))' conditions.push(start_date, start_date, start_date) end - Employment.where(conditions).count > 0 + Employment.where(conditions).count.positive? end end diff --git a/app/models/employment_role.rb b/app/models/employment_role.rb index 4d4e35229..6e1a9bc4c 100644 --- a/app/models/employment_role.rb +++ b/app/models/employment_role.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -14,7 +16,7 @@ # employment_role_category_id :integer # -class EmploymentRole < ActiveRecord::Base +class EmploymentRole < ApplicationRecord belongs_to :employment_role_category, optional: true has_many :employment_roles_employments, dependent: :restrict_with_exception diff --git a/app/models/employment_role_category.rb b/app/models/employment_role_category.rb index 961768342..25be46f3d 100644 --- a/app/models/employment_role_category.rb +++ b/app/models/employment_role_category.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -11,7 +13,7 @@ # name :string not null # -class EmploymentRoleCategory < ActiveRecord::Base +class EmploymentRoleCategory < ApplicationRecord has_many :employment_roles, dependent: :restrict_with_exception validates_by_schema diff --git a/app/models/employment_role_level.rb b/app/models/employment_role_level.rb index b0548d659..0bcb0228e 100644 --- a/app/models/employment_role_level.rb +++ b/app/models/employment_role_level.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -11,7 +13,7 @@ # name :string not null # -class EmploymentRoleLevel < ActiveRecord::Base +class EmploymentRoleLevel < ApplicationRecord has_many :employment_roles_employments, dependent: :restrict_with_exception validates_by_schema diff --git a/app/models/employment_roles_employment.rb b/app/models/employment_roles_employment.rb index 5fe04f050..abd633752 100644 --- a/app/models/employment_roles_employment.rb +++ b/app/models/employment_roles_employment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -14,7 +16,9 @@ # percent :decimal(5, 2) not null # -class EmploymentRolesEmployment < ActiveRecord::Base +class EmploymentRolesEmployment < ApplicationRecord + has_paper_trail(meta: { employee_id: ->(e) { e.employment.employee_id } }, skip: [:id]) + belongs_to :employment belongs_to :employment_role belongs_to :employment_role_level, optional: true @@ -29,7 +33,7 @@ def to_s '' end - "#{employment_role}#{level} #{sprintf('%g',percent)}%" + "#{employment_role}#{level} #{format('%g', percent)}%" end private diff --git a/app/models/expense.rb b/app/models/expense.rb index bd8d45a75..5050a910f 100644 --- a/app/models/expense.rb +++ b/app/models/expense.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # == Schema Information # # Table name: expenses @@ -17,15 +19,15 @@ # submission_date :date # -class Expense < ActiveRecord::Base +class Expense < ApplicationRecord belongs_to :order belongs_to :employee belongs_to :reviewer, class_name: 'Employee' has_one_attached :receipt - enum kind: %i(project training sales support other) - enum status: %i(pending deferred approved rejected) + enum kind: { project: 0, training: 1, sales: 2, support: 3, other: 4 } + enum status: { pending: 0, deferred: 1, approved: 2, rejected: 3 } validates_by_schema @@ -56,20 +58,20 @@ def assert_payment_month def self.reimbursement_months statement = 'SELECT DISTINCT ' \ - 'EXTRACT(YEAR FROM reimbursement_date)::int AS year, ' \ - 'EXTRACT(MONTH FROM reimbursement_date)::int AS month ' \ - 'FROM expenses ' \ - 'WHERE reimbursement_date IS NOT NULL ' \ - 'ORDER BY year, month' + 'EXTRACT(YEAR FROM reimbursement_date)::int AS year, ' \ + 'EXTRACT(MONTH FROM reimbursement_date)::int AS month ' \ + 'FROM expenses ' \ + 'WHERE reimbursement_date IS NOT NULL ' \ + 'ORDER BY year, month' connection.select_rows(statement).collect { |year, month| Date.new(year, month, 1) } end def self.payment_years(employee) statement = 'SELECT DISTINCT ' \ - 'EXTRACT(YEAR FROM payment_date)::int AS year ' \ - 'FROM expenses ' \ - "WHERE employee_id = #{employee.id} " \ - 'ORDER BY year' + 'EXTRACT(YEAR FROM payment_date)::int AS year ' \ + 'FROM expenses ' \ + "WHERE employee_id = #{employee.id} " \ + 'ORDER BY year' connection.select_values(statement).collect { |year| Date.new(year, 1, 1) } end diff --git a/app/models/holiday.rb b/app/models/holiday.rb index 028d08a22..b01dff222 100644 --- a/app/models/holiday.rb +++ b/app/models/holiday.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -12,12 +14,12 @@ # musthours_day :float not null # -class Holiday < ActiveRecord::Base +class Holiday < ApplicationRecord include ActionView::Helpers::NumberHelper include Comparable - after_save :clear_cache after_destroy :clear_cache + after_save :clear_cache validates_by_schema validates :holiday_date, @@ -47,7 +49,7 @@ def musttime(date) end def holiday?(date) - cached.keys.include?(date) + cached.key?(date) end # 0 is Sunday, 6 is Saturday @@ -69,15 +71,15 @@ def holidays(period) def cached RequestStore.store[model_name.route_key] ||= Rails.cache.fetch(model_name.route_key) do - Hash[Holiday.order('holiday_date'). - reject { |h| weekend?(h.holiday_date) }. - collect { |h| [h.holiday_date, h.musthours_day] }] + Holiday.order('holiday_date') + .reject { |h| weekend?(h.holiday_date) } + .to_h { |h| [h.holiday_date, h.musthours_day] } end end def clear_cache RequestStore.store[model_name.route_key] = nil - Rails.cache.clear(model_name.route_key) + Rails.cache.delete(model_name.route_key) true end @@ -87,8 +89,8 @@ def workday_hours(period, must_hours_per_day) length = period.length weeks = length / 7 hours = weeks * 5 * must_hours_per_day - if length % 7 > 0 - last_period = Period.new(period.start_date + weeks * 7, period.end_date) + if (length % 7).positive? + last_period = Period.new(period.start_date + (weeks * 7), period.end_date) last_period.step do |day| hours += must_hours_per_day unless weekend?(day) end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 94cf00177..6418a62eb 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -25,9 +27,9 @@ # class Invoice < ActiveRecord::Base - STATUSES = %w(draft sent paid partially_paid dept_collection cancelled deleted unknown).freeze + STATUSES = %w(draft sent paid partially_paid cancelled deleted unknown).freeze - enum grouping: %w(accounting_posts employees manual) + enum grouping: { 'accounting_posts' => 0, 'employees' => 1, 'manual' => 2 } belongs_to :order belongs_to :billing_address @@ -49,12 +51,12 @@ class Invoice < ActiveRecord::Base before_validation :generate_reference, on: :create before_validation :generate_due_date before_validation :update_totals - before_save :save_remote_invoice, if: -> { Invoicing.instance.present? } - before_save :assign_worktimes before_create :lock_client_invoice_number after_create :update_client_invoice_number - after_destroy :delete_remote_invoice, if: -> { Invoicing.instance.present? } after_save :update_order_billing_address + before_save :save_remote_invoice, if: -> { Invoicing.instance.present? } + before_save :assign_worktimes + after_destroy :delete_remote_invoice, if: -> { Invoicing.instance.present? } protect_if :paid?, 'Bezahlte Rechnungen können nicht gelöscht werden.' protect_if :order_closed?, 'Rechnungen von geschlossenen Aufträgen können nicht gelöscht werden.' @@ -63,9 +65,7 @@ class Invoice < ActiveRecord::Base def title title = order.name - if order.contract && order.contract.number? - title += " gemäss Vertrag #{order.contract.number}" - end + title += " gemäss Vertrag #{order.contract.number}" if order.contract&.number? title end @@ -90,7 +90,7 @@ def manual_invoice? end def calculated_total_amount - total = positions.collect(&:total_amount).sum + total = positions.sum(&:total_amount) round_to_5_cents(total) end @@ -113,7 +113,7 @@ def destroyable? end STATUSES.each do |s| - define_method("#{s}?") do + define_method(:"#{s}?") do status == s end end @@ -138,7 +138,7 @@ def manual_position def accounting_post_positions worktimes.group(:work_item_id).sum(:hours).collect do |work_item_id, hours| - post = AccountingPost.find_by!(work_item_id: work_item_id) + post = AccountingPost.find_by!(work_item_id:) Invoicing::Position.new(post, hours) end.sort_by(&:name) end @@ -152,11 +152,11 @@ def employee_positions end def worktimes - Ordertime.in_period(period). - where(billable: true). - where(work_item_id: work_item_ids). - where(employee_id: employee_ids). - where(invoice_id: [id, nil]) + Ordertime.in_period(period) + .where(billable: true) + .where(work_item_id: work_item_ids) + .where(employee_id: employee_ids) + .where(invoice_id: [id, nil]) end def lock_client_invoice_number @@ -169,9 +169,9 @@ def update_client_invoice_number end def update_order_billing_address - if order.billing_address_id != billing_address_id - order.update_column(:billing_address_id, billing_address_id) - end + return unless order.billing_address_id != billing_address_id + + order.update_column(:billing_address_id, billing_address_id) end def update_totals @@ -179,7 +179,7 @@ def update_totals self.total_hours = 0 self.total_amount = calculated_total_amount if grouping_changed? else - self.total_hours = positions.collect(&:total_hours).sum + self.total_hours = positions.sum(&:total_hours) self.total_amount = calculated_total_amount end end @@ -204,32 +204,28 @@ def set_default_status end def assert_positive_period - if period_to && period_from && period_to < period_from - errors.add(:period_to, 'muss nach von sein.') - end + return unless period_to && period_from && period_to < period_from + + errors.add(:period_to, 'muss nach von sein.') end def assert_order_has_contract - unless order.contract - errors.add(:order_id, 'muss einen definierten Vertrag haben.') - end + return if order.contract + + errors.add(:order_id, 'muss einen definierten Vertrag haben.') end def assert_order_not_closed - if order_closed? - errors.add(:order, 'darf nicht geschlossen sein.') - end + return unless order_closed? + + errors.add(:order, 'darf nicht geschlossen sein.') end def save_remote_invoice self.invoicing_key = Invoicing.instance.save_invoice(self, positions) rescue Invoicing::Error => e errors.add(:base, "Fehler im Invoicing Service: #{e.message}") - Rails.logger.error <<~ERROR - #{e.class.name}: #{e.message} - #{e.data.inspect} - #{e.backtrace.join("\n")} - ERROR + Rails.logger.error(e.class.name + ': ' + e.message + "\n" + e.backtrace.join("\n")) throw :abort end @@ -239,7 +235,7 @@ def delete_remote_invoice # Ignore "no rights / not found" errors, the invoice does not exist remotly in this case. unless e.code == 15_016 errors.add(:base, "Fehler im Invoicing Service: #{e.message}") - Rails.logger.error(e.class.name + ': ' + e.message + "\n" + e.backtrace.join("\n")) + Rails.logger.error("#{e.class.name}: #{e.message}\n#{e.backtrace.join("\n")}") raise ActiveRecord::Rollback end end diff --git a/app/models/order.rb b/app/models/order.rb index bfdee48bb..9f2ef025f 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -25,7 +27,7 @@ # major_chance_value :integer # -class Order < ActiveRecord::Base +class Order < ApplicationRecord include BelongingToWorkItem include Closable include Evaluatable @@ -72,13 +74,13 @@ class Order < ActiveRecord::Base ### CALLBACKS - before_update :set_closed_at - before_validation :set_self_in_nested after_initialize :set_default_status_id + before_validation :set_self_in_nested after_create :create_order_targets + before_update :set_closed_at scope :minimal, lambda { - select('orders.id, work_items.name, work_items.path_names, work_items.path_shortnames') + select('orders.id, orders.status_id, orders.work_item_id, work_items.name, work_items.path_names, work_items.path_shortnames') } scope :open, -> { where(status: OrderStatus.open) } @@ -86,9 +88,9 @@ class Order < ActiveRecord::Base class << self def order_by_target_scope(target_scope_id, desc = false) joins('LEFT JOIN order_targets sort_target ' \ - 'ON sort_target.order_id = orders.id '). - where('sort_target.target_scope_id = ? OR sort_target.id IS NULL', target_scope_id). - reorder("sort_target.rating #{desc ? 'asc' : 'desc'}") + 'ON sort_target.order_id = orders.id ') + .where('sort_target.target_scope_id = ? OR sort_target.id IS NULL', target_scope_id) + .reorder("sort_target.rating #{desc ? 'asc' : 'desc'}") end end @@ -118,11 +120,11 @@ def label end def default_billing_address_id - billing_address_id || client.billing_addresses.list.pluck(:id).first + billing_address_id || client.billing_addresses.list.pick(:id) end def set_default_status_id - self.status_id ||= OrderStatus.defaults.pluck(:id).first + self.status_id ||= OrderStatus.defaults.pick(:id) end def major_risk @@ -140,9 +142,9 @@ def label_with_workitem_path private def work_item_parent_presence - if work_item && work_item.parent_id.nil? - errors.add(:base, 'Kunde darf nicht leer sein') - end + return unless work_item && work_item.parent_id.nil? + + errors.add(:base, 'Kunde darf nicht leer sein') end def set_self_in_nested @@ -151,9 +153,7 @@ def set_self_in_nested # don't try to set self in frozen nested attributes (-> marked for destroy) [order_team_members, order_contacts].each do |c| c.each do |e| - unless e.frozen? - e.order = self - end + e.order = self unless e.frozen? end end end diff --git a/app/models/order_chance.rb b/app/models/order_chance.rb index a64dc02cc..268a07a58 100644 --- a/app/models/order_chance.rb +++ b/app/models/order_chance.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -28,7 +30,6 @@ def update_major_order_value def major_order_value order.order_chances .where(type: OrderChance.sti_name) - .pluck('MAX(probability * impact)') - .first + .pick(Arel.sql('MAX(probability * impact)')) end end diff --git a/app/models/order_comment.rb b/app/models/order_comment.rb index 5f45da52f..0c9b2e706 100644 --- a/app/models/order_comment.rb +++ b/app/models/order_comment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -16,7 +18,7 @@ # updated_at :datetime # -class OrderComment < ActiveRecord::Base +class OrderComment < ApplicationRecord ### ASSOCIATIONS belongs_to :order @@ -26,7 +28,6 @@ class OrderComment < ActiveRecord::Base ### VALIDATIONS validates_by_schema - validates :creator, :updater, presence: true ### SCOPES diff --git a/app/models/order_contact.rb b/app/models/order_contact.rb index f0998d0d7..38f9b4402 100644 --- a/app/models/order_contact.rb +++ b/app/models/order_contact.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -13,7 +15,7 @@ # comment :string # -class OrderContact < ActiveRecord::Base +class OrderContact < ApplicationRecord belongs_to :contact belongs_to :order @@ -42,17 +44,17 @@ def contact_id_or_crm=(value) private def create_crm_contact - if Crm.instance && contact_id_or_crm.to_s.start_with?(Contact::CRM_ID_PREFIX) - crm_key = contact_id_or_crm.sub(Contact::CRM_ID_PREFIX, '') - person = Crm.instance.find_person(crm_key) - self.contact_id = nil - build_contact(person.merge(client_id: order.client.id)) if person - end + return unless Crm.instance && contact_id_or_crm.to_s.start_with?(Contact::CRM_ID_PREFIX) + + crm_key = contact_id_or_crm.sub(Contact::CRM_ID_PREFIX, '') + person = Crm.instance.find_person(crm_key) + self.contact_id = nil + build_contact(person.merge(client_id: order.client.id)) if person end def assert_contact_from_same_client - if contact && order && contact.client_id != order.client.id - errors.add(:contact_id, 'muss zum selben Kunden wie der Auftrag gehören.') - end + return unless contact && order && contact.client_id != order.client.id + + errors.add(:contact_id, 'muss zum selben Kunden wie der Auftrag gehören.') end end diff --git a/app/models/order_kind.rb b/app/models/order_kind.rb index 6806a6938..28ae72dcf 100644 --- a/app/models/order_kind.rb +++ b/app/models/order_kind.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -11,7 +13,7 @@ # name :string not null # -class OrderKind < ActiveRecord::Base +class OrderKind < ApplicationRecord has_many :orders, foreign_key: :kind_id validates_by_schema diff --git a/app/models/order_risk.rb b/app/models/order_risk.rb index c59698a23..667e07e65 100644 --- a/app/models/order_risk.rb +++ b/app/models/order_risk.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -27,7 +29,6 @@ def update_major_order_value def major_order_value order.order_risks - .pluck('MAX(probability * impact)') - .first + .pick(Arel.sql('MAX(probability * impact)')) end end diff --git a/app/models/order_status.rb b/app/models/order_status.rb index d05a8cca3..2cb8f9e65 100644 --- a/app/models/order_status.rb +++ b/app/models/order_status.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -15,8 +17,8 @@ # default :boolean default(FALSE), not null # -class OrderStatus < ActiveRecord::Base - STYLES = %w(default success info warning danger).freeze +class OrderStatus < ApplicationRecord + STYLES = %w[default success info warning danger].freeze include Closable diff --git a/app/models/order_target.rb b/app/models/order_target.rb index bffe23ede..c4fbf4da8 100644 --- a/app/models/order_target.rb +++ b/app/models/order_target.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -16,8 +18,8 @@ # updated_at :datetime # -class OrderTarget < ActiveRecord::Base - RATINGS = %w(green orange red).freeze +class OrderTarget < ApplicationRecord + RATINGS = %w[green orange red].freeze belongs_to :order belongs_to :target_scope @@ -28,9 +30,9 @@ class OrderTarget < ActiveRecord::Base validates :target_scope_id, uniqueness: { scope: :order_id } scope :list, lambda { - includes(:target_scope). - references(:target_scope). - order('target_scopes.position') + includes(:target_scope) + .references(:target_scope) + .order('target_scopes.position') } def target_critical? diff --git a/app/models/order_team_member.rb b/app/models/order_team_member.rb index 9a2b3d8f9..49f828726 100644 --- a/app/models/order_team_member.rb +++ b/app/models/order_team_member.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -13,7 +15,7 @@ # comment :string # -class OrderTeamMember < ActiveRecord::Base +class OrderTeamMember < ApplicationRecord belongs_to :employee belongs_to :order diff --git a/app/models/order_uncertainty.rb b/app/models/order_uncertainty.rb index fd4af9ea8..a7943c013 100644 --- a/app/models/order_uncertainty.rb +++ b/app/models/order_uncertainty.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -18,7 +20,7 @@ # updated_at :datetime not null # -class OrderUncertainty < ActiveRecord::Base +class OrderUncertainty < ApplicationRecord MEDIUM_THRESHOLD = 3 HIGH_THRESHOLD = 8 @@ -40,10 +42,10 @@ class OrderUncertainty < ActiveRecord::Base validates_by_schema - after_save :update_major_order_value after_destroy :update_major_order_value + after_save :update_major_order_value - scope :list, -> { order('probability * impact DESC') } + scope :list, -> { order(Arel.sql('probability * impact DESC')) } class << self def risk(value) diff --git a/app/models/ordertime.rb b/app/models/ordertime.rb index 3875046ab..21a5ead71 100644 --- a/app/models/ordertime.rb +++ b/app/models/ordertime.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -26,7 +28,7 @@ class Ordertime < Worktime self.account_label = 'Position' - alias_attribute :account, :work_item + alias account work_item alias_attribute :account_id, :work_item_id validates_by_schema @@ -42,7 +44,7 @@ def account_id=(value) end def order - work_item.accounting_post && work_item.accounting_post.order + work_item.accounting_post&.order end def amount @@ -50,9 +52,9 @@ def amount end def work_item_closed? - (work_item && work_item.closed?) || + work_item&.closed? || (work_item_id_was && work_item_id_was != work_item_id && - WorkItem.where(id: work_item_id_was, closed: true).exists?) + WorkItem.exists?(id: work_item_id_was, closed: true)) end def invoice_sent_or_paid? @@ -64,21 +66,21 @@ def invoice_sent_or_paid? ########### validation helpers ########### def validate_by_work_item - if work_item && work_item.accounting_post - work_item.accounting_post.validate_worktime(self) - end + return unless work_item&.accounting_post + + work_item.accounting_post.validate_worktime(self) end def validate_accounting_post - if work_item && !work_item.accounting_post - errors.add(:work_item_id, 'Der Auftrag hat keine Buchungsposition.') - end + return unless work_item && !work_item.accounting_post + + errors.add(:work_item_id, 'Der Auftrag hat keine Buchungsposition.') end def validate_work_item_open - if changed != %w(invoice_id) && work_item_closed? - errors.add(:base, 'Auf geschlossene Aufträge und/oder Positionen kann nicht gebucht werden.') - end + return unless changed != %w[invoice_id] && work_item_closed? + + errors.add(:base, 'Auf geschlossene Aufträge und/oder Positionen kann nicht gebucht werden.') end def protect_work_item_closed diff --git a/app/models/overtime_vacation.rb b/app/models/overtime_vacation.rb index 17477981c..683516d5e 100644 --- a/app/models/overtime_vacation.rb +++ b/app/models/overtime_vacation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -13,7 +15,7 @@ # transfer_date :date not null # -class OvertimeVacation < ActiveRecord::Base +class OvertimeVacation < ApplicationRecord belongs_to :employee validates_by_schema diff --git a/app/models/planning.rb b/app/models/planning.rb index 310faa88b..ea5f05d10 100644 --- a/app/models/planning.rb +++ b/app/models/planning.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -15,7 +17,7 @@ # definitive :boolean default(FALSE), not null # -class Planning < ActiveRecord::Base +class Planning < ApplicationRecord validates_by_schema validate :date_must_be_weekday @@ -45,15 +47,15 @@ def to_s def order @order ||= Order.joins('LEFT JOIN work_items ON ' \ - 'orders.work_item_id = ANY (work_items.path_ids)'). - find_by('work_items.id = ?', work_item_id) + 'orders.work_item_id = ANY (work_items.path_ids)') + .find_by('work_items.id = ?', work_item_id) end private def date_must_be_weekday - if date.saturday? || date.sunday? - errors.add(:weekday, 'muss ein Werktag sein') - end + return unless date.saturday? || date.sunday? + + errors.add(:weekday, 'muss ein Werktag sein') end end diff --git a/app/models/portfolio_item.rb b/app/models/portfolio_item.rb index b02852a29..230889fb9 100644 --- a/app/models/portfolio_item.rb +++ b/app/models/portfolio_item.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -12,7 +14,7 @@ # active :boolean default(TRUE), not null # -class PortfolioItem < ActiveRecord::Base +class PortfolioItem < ApplicationRecord has_many :accounting_posts scope :list, -> { order(:name) } diff --git a/app/models/sector.rb b/app/models/sector.rb index 23837fa0c..89d7994e4 100644 --- a/app/models/sector.rb +++ b/app/models/sector.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -12,7 +14,7 @@ # active :boolean default(TRUE), not null # -class Sector < ActiveRecord::Base +class Sector < ApplicationRecord has_many :clients, dependent: :nullify scope :list, -> { order(:name) } diff --git a/app/models/service.rb b/app/models/service.rb index 182274158..b7a27029c 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -12,7 +14,7 @@ # active :boolean default(TRUE), not null # -class Service < ActiveRecord::Base +class Service < ApplicationRecord has_many :accounting_posts scope :list, -> { order(:name) } diff --git a/app/models/target_scope.rb b/app/models/target_scope.rb index 6b153ef2b..74fe02eaf 100644 --- a/app/models/target_scope.rb +++ b/app/models/target_scope.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -16,7 +18,7 @@ # rating_red_description :string # -class TargetScope < ActiveRecord::Base +class TargetScope < ApplicationRecord has_many :order_targets, dependent: :destroy validates_by_schema diff --git a/app/models/user_notification.rb b/app/models/user_notification.rb index f3847e3ed..fddc6c95e 100644 --- a/app/models/user_notification.rb +++ b/app/models/user_notification.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -13,7 +15,7 @@ # message :text not null # -class UserNotification < ActiveRecord::Base +class UserNotification < ApplicationRecord include Comparable # Validation helpers @@ -24,17 +26,16 @@ class UserNotification < ActiveRecord::Base scope :list, -> { order('date_from DESC, date_to DESC') } class << self - def list_during(period = nil, current_user = nil) + def list_during(period = nil, _current_user = nil) # only show notifications for the current week return if period period = Period.current_week custom = list.where('date_from BETWEEN ? AND ? OR date_to BETWEEN ? AND ?', period.start_date, period.end_date, - period.start_date, period.end_date). - reorder('date_from') + period.start_date, period.end_date) + .reorder('date_from') list = custom.to_a.concat(holiday_notifications(period)) - list.push(worktimes_commit_notification) if show_worktimes_commit_notification?(current_user) list.sort! end @@ -44,26 +45,8 @@ def holiday_notifications(period = nil) regular.collect! { |holiday| new_holiday_notification(holiday) } end - def show_worktimes_commit_notification?(employee) - today = Time.zone.today - - committed = employee && employee.committed_worktimes_at - config = Settings.committed_worktimes.notification - - show_range = today.at_end_of_month - today < config.days_at_end_of_month || - today - today.at_beginning_of_month < config.days_at_beginning_of_month - - !(committed && today - committed <= config.days_at_beginning_of_month) && show_range - end - private - def worktimes_commit_notification - new(date_from: Time.zone.today, - date_to: Time.zone.today, - message: 'Bitte Zeiten bis spätestens am ersten Arbeitstag des Monats freigeben.') - end - def new_holiday_notification(holiday) new(date_from: holiday.holiday_date, date_to: holiday.holiday_date, @@ -71,9 +54,10 @@ def new_holiday_notification(holiday) end def holiday_message(holiday) - I18n.l(holiday.holiday_date, format: :long) + - ' ist ein Feiertag (' + format('%01.2f', holiday.musthours_day).to_s + - ' Stunden Sollarbeitszeit)' + date = I18n.l(holiday.holiday_date, format: :long) + expected_hours = format('%01.2f', holiday.musthours_day) + + "#{date} ist ein Feiertag (#{expected_hours} Stunden Sollarbeitszeit)" end end @@ -90,8 +74,8 @@ def to_s private def validate_period - if date_from && date_to && date_from > date_to - errors.add(:date_to, 'Enddatum muss nach Startdatum sein.') - end + return unless date_from && date_to && date_from > date_to + + errors.add(:date_to, 'Enddatum muss nach Startdatum sein.') end end diff --git a/app/models/util/employee_statistics.rb b/app/models/util/employee_statistics.rb index fab35f854..0ae799375 100644 --- a/app/models/util/employee_statistics.rb +++ b/app/models/util/employee_statistics.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -10,6 +12,24 @@ def initialize(employee) @employee = employee end + ######### employment percent information ############ + + def percents_at(date) + employee.employment_at(date)&.percent || 0 + end + + def billable_percents_at(date) + employee.employment_at(date)&.employment_roles_employments&.select do |e| + e.employment_role.billable + end&.sum(&:percent) || 0 + end + + def average_percents(period) + employments_during(period).sum do |employment| + employment.period.length.to_f / period.length * employment.percent + end + end + ######### vacation information ############ # Returns the unused days of vacation remaining until the end of the current year. @@ -38,10 +58,10 @@ def used_vacations(period) return 0 if period.nil? WorkingCondition.sum_with(:must_hours_per_day, period) do |p, hours| - @employee.worktimes.in_period(p). - joins(:absence). - where(absences: { vacation: true }). - sum(:hours).to_f / hours + @employee.worktimes.in_period(p) + .joins(:absence) + .where(absences: { vacation: true }) + .sum(:hours).to_f / hours end end @@ -50,7 +70,7 @@ def used_vacations(period) def absences(period, payed = nil) worktimes = @employee.worktimes.joins(:absence).in_period(period) - worktimes = worktimes.where(absences: { payed: payed }) if payed.in? [true, false] + worktimes = worktimes.where(absences: { payed: }) if payed.in? [true, false] worktimes.sum(:hours).to_f end @@ -94,39 +114,39 @@ def pending_worktime(period) # Returns the hours this employee worked plus the payed absences for the given period. def payed_worktime(period) - @employee.worktimes. - joins('LEFT JOIN absences ON absences.id = absence_id'). - in_period(period). - where('((work_item_id IS NOT NULL AND absence_id IS NULL) OR absences.payed)'). - sum(:hours). - to_f + @employee.worktimes + .joins('LEFT JOIN absences ON absences.id = absence_id') + .in_period(period) + .where('((work_item_id IS NOT NULL AND absence_id IS NULL) OR absences.payed)') + .sum(:hours) + .to_f end def unpaid_absences(period) - @employee.worktimes. - joins('LEFT JOIN absences ON absences.id = absence_id'). - in_period(period). - where('NOT (absences.payed)'). - sum(:hours). - to_f + @employee.worktimes + .joins('LEFT JOIN absences ON absences.id = absence_id') + .in_period(period) + .where('NOT (absences.payed)') + .sum(:hours) + .to_f end # Return the overtime days that were transformed into vacations up to the given date. def overtime_vacation_days(period) WorkingCondition.sum_with(:must_hours_per_day, period) do |p, hours| - @employee.overtime_vacations. - where('transfer_date BETWEEN ? AND ?', p.start_date, p.end_date). - sum(:hours). - to_f / hours + @employee.overtime_vacations + .where('transfer_date BETWEEN ? AND ?', p.start_date, p.end_date) + .sum(:hours) + .to_f / hours end end # Return the overtime hours that were transformed into vacations up to the given date. def overtime_vacation_hours(date = nil) - @employee.overtime_vacations. - where(date ? ['transfer_date <= ?', date] : nil). - sum(:hours). - to_f + @employee.overtime_vacations + .where(date ? ['transfer_date <= ?', date] : nil) + .sum(:hours) + .to_f end ######### employment helpers ###################### diff --git a/app/models/util/id_value.rb b/app/models/util/id_value.rb index c571c45a4..457586884 100644 --- a/app/models/util/id_value.rb +++ b/app/models/util/id_value.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/app/models/util/ldap_authenticator.rb b/app/models/util/ldap_authenticator.rb index 26fbb68a2..72f66635c 100644 --- a/app/models/util/ldap_authenticator.rb +++ b/app/models/util/ldap_authenticator.rb @@ -28,7 +28,7 @@ def auth_user(base_dn) connection.bind_as( base: base_dn, filter: "uid=#{username}", - password: password + password: ) end @@ -36,15 +36,15 @@ def auth_user(base_dn) def all_users connection.search( base: internal_dn, - attributes: %w(uid sn givenname mail) + attributes: %w[uid sn givenname mail] ) end private def valid_user? - return if username.strip.empty? - return unless internal_user? || external_user? + return false if username.strip.empty? + return false unless internal_user? || external_user? true end @@ -92,7 +92,7 @@ def config def tls_options { verify_mode: OpenSSL::SSL::VERIFY_PEER, - cert_store: cert_store + cert_store: } end diff --git a/app/models/util/period.rb b/app/models/util/period.rb index 0db3a3960..c073815c6 100644 --- a/app/models/util/period.rb +++ b/app/models/util/period.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -68,6 +70,11 @@ def past_month(date = Time.zone.today, options = {}) new(date - 28, date + 7, options[:label], options[:shortcut]) end + def previous_month(date = Time.zone.today) + date = date.to_date if date.is_a?(Time) + new(date.beginning_of_month - 1.month, date.beginning_of_month - 1.day) + end + def coming_month(date = Time.zone.today, options = {}) date = date.to_date if date.is_a?(Time) date -= (date.wday - 1) % 7 @@ -94,27 +101,25 @@ def parse(shortcut) range, shift = parse_shortcut(shortcut) now = Time.zone.now case range - when 'd' then day_for(now.advance(days: shift), shortcut: shortcut) - when 'w' then week_for(now.advance(days: shift * 7), shortcut: shortcut) - when 'm' then month_for(now.advance(months: shift), shortcut: shortcut) + when 'd' then day_for(now.advance(days: shift), shortcut:) + when 'w' then week_for(now.advance(days: shift * 7), shortcut:) + when 'm' then month_for(now.advance(months: shift), shortcut:) when 'M' then next_n_months(shift, now) - when 'q' then quarter_for(now.advance(months: shift * 3), shortcut: shortcut) + when 'q' then quarter_for(now.advance(months: shift * 3), shortcut:) when 'Q' then parse_year_quarter(now.year, shift, shortcut) - when 'y' then year_for(now.advance(years: shift), shortcut: shortcut) + when 'y' then year_for(now.advance(years: shift), shortcut:) when 'b' then business_year_for(now.to_date) end end def parse_year_quarter(year, shift, shortcut) - if [1, 2, 3, 4].exclude?(shift) - raise ArgumentError, 'Unsupported shift for quarter shortcut' - end + raise ArgumentError, 'Unsupported shift for quarter shortcut' if [1, 2, 3, 4].exclude?(shift) - quarter_for(Date.civil(year, shift * 3, 1), shortcut: shortcut) + quarter_for(Date.civil(year, shift * 3, 1), shortcut:) end # Build a period, even with illegal arguments - def with(start_date = Time.zone.today, end_date = Time.zone_today, label = nil) + def with(start_date = Time.zone.today, end_date = Time.zone.today, label = nil) # rubocop:disable Style/RescueModifier start_date = parse_date(start_date) rescue nil end_date = parse_date(end_date) rescue nil @@ -128,7 +133,7 @@ def parse_date(date) if date.is_a? String begin date = Date.strptime(date, I18n.t('date.formats.default')) - rescue + rescue StandardError date = Date.parse(date) end end @@ -158,7 +163,7 @@ def month_label(date) end def quarter_label(date) - "#{date.month / 4 + 1}. Quartal" + "#{(date.month / 4) + 1}. Quartal" end def year_label(date) @@ -170,7 +175,7 @@ def next_n_months_label(n) end def parse_shortcut(shortcut) - range = shortcut[-1..-1] + range = shortcut[-1..] shift = shortcut[0..-2].to_i if range != '0' [range, shift] end @@ -216,10 +221,10 @@ def &(other) Period.new(new_start_date, new_end_date) end - def step(size = 1) - @start_date.step(@end_date, size) do |date| - yield date - end + def step(size = 1, &) + return @start_date.step(@end_date, size) unless block_given? + + @start_date.step(@end_date, size, &) end def step_months @@ -252,7 +257,7 @@ def negative? end def url_query_s - @url_query ||= 'start_date=' + start_date.to_s + '&end_date=' + end_date.to_s + @url_query ||= "start_date=#{start_date}&end_date=#{end_date}" end def limited? @@ -268,7 +273,7 @@ def ==(other) end def hash - 37 * start_date.hash ^ 43 * end_date.hash + (37 * start_date.hash) ^ (43 * end_date.hash) end def to_s @@ -276,12 +281,12 @@ def to_s if @start_date == @end_date I18n.l(@start_date) else - I18n.l(@start_date) + ' - ' + I18n.l(@end_date) + "#{I18n.l(@start_date)} - #{I18n.l(@end_date)}" end elsif @start_date - I18n.l(@start_date) + ' - egal' + "#{I18n.l(@start_date)} - egal" elsif @end_date - 'egal - ' + I18n.l(@end_date) + "egal - #{I18n.l(@end_date)}" else 'egal - egal' end diff --git a/app/models/util/report_type.rb b/app/models/util/report_type.rb index bf5d80572..b0fe9fa59 100644 --- a/app/models/util/report_type.rb +++ b/app/models/util/report_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -6,12 +8,25 @@ class ReportType include Comparable - attr_reader :key, :name, :accuracy + attr_accessor :key, :name, :accuracy START_STOP = false + def self.instances + [ + ReportType::StartStopType::INSTANCE, + ReportType::HoursDayType::INSTANCE, + ReportType::HoursWeekType::INSTANCE, + ReportType::HoursMonthType::INSTANCE + ].freeze + end + + def self.all_instances + instances + [ReportType::AutoStartType::INSTANCE].freeze + end + def self.[](key) - ALL_INSTANCES.each { |type| return type if type.key == key } + all_instances.find { |type| type.key == key.to_s } end def to_s @@ -40,18 +55,6 @@ def date_string(date) I18n.l(date, format: :long) end - module Accessors - def report_type - type = self['report_type'] - type.is_a?(String) ? ReportType[type] : type - end - - def report_type=(type) - type = type.key if type.is_a? ReportType - self['report_type'] = type - end - end - protected def initialize(key, name, accuracy) @@ -67,106 +70,3 @@ def rounded_hours(worktime) "#{hours}:#{minutes}".html_safe end end - -class StartStopType < ReportType - INSTANCE = new 'start_stop_day', 'Von/Bis Zeit', 10 - START_STOP = true - - def time_string(worktime) - if worktime.from_start_time.is_a?(Time) && - worktime.to_end_time.is_a?(Time) - I18n.l(worktime.from_start_time, format: :time) + ' - ' + - I18n.l(worktime.to_end_time, format: :time) + - ' (' + rounded_hours(worktime) + ' h)' - end - end - - def copy_times(source, target) - super source, target - target.from_start_time = source.from_start_time - target.to_end_time = source.to_end_time - end - - def validate_worktime(worktime) - unless worktime.from_start_time.is_a?(Time) - worktime.errors.add(:from_start_time, 'Die Anfangszeit ist ungültig') - end - unless worktime.to_end_time.is_a?(Time) - worktime.errors.add(:to_end_time, 'Die Endzeit ist ungültig') - end - if worktime.from_start_time.is_a?(Time) && worktime.to_end_time.is_a?(Time) && - worktime.to_end_time <= worktime.from_start_time - worktime.errors.add(:to_end_time, 'Die Endzeit muss nach der Startzeit sein') - end - if worktime.from_start_time&.to_date != worktime.to_end_time&.to_date - worktime.errors.add(:to_end_time, 'Die Endzeit muss zwischen 00:00-23:59 liegen') - end - end -end - -class AutoStartType < StartStopType - INSTANCE = new 'auto_start', 'Von/Bis offen', 12 - - def time_string(worktime) - if worktime.from_start_time.is_a?(Time) - 'Start um ' + I18n.l(worktime.from_start_time, format: :time) - end - end - - def validate_worktime(worktime) - # set defaults - worktime.work_date = Time.zone.today - worktime.hours = 0 - worktime.to_end_time = nil - # validate - unless worktime.from_start_time.is_a?(Time) - worktime.errors.add(:from_start_time, 'Die Anfangszeit ist ungültig') - end - if worktime.employee - existing = worktime.employee.send("running_#{worktime.class.name[0..-5].downcase}".to_sym) - if existing && existing != worktime - worktime.errors.add(:employee_id, "Es wurde bereits eine offene #{worktime.class.model_name.human} erfasst") - end - end - end -end - -class HoursDayType < ReportType - INSTANCE = new 'absolute_day', 'Stunden/Tag', 6 - - def time_string(worktime) - rounded_hours(worktime) + ' h' - end -end - -class HoursWeekType < ReportType - INSTANCE = new 'week', 'Stunden/Woche', 4 - - def time_string(worktime) - rounded_hours(worktime) + ' h in dieser Woche' - end - - def date_string(date) - I18n.l(date, format: 'W %V, %Y') - end -end - -class HoursMonthType < ReportType - INSTANCE = new 'month', 'Stunden/Monat', 2 - - def time_string(worktime) - rounded_hours(worktime) + ' h in diesem Monat' - end - - def date_string(date) - I18n.l(date, format: '%m.%Y') - end -end - -class ReportType - INSTANCES = [StartStopType::INSTANCE, - HoursDayType::INSTANCE, - HoursWeekType::INSTANCE, - HoursMonthType::INSTANCE].freeze - ALL_INSTANCES = INSTANCES + [AutoStartType::INSTANCE] -end diff --git a/app/models/util/report_type/accessors.rb b/app/models/util/report_type/accessors.rb new file mode 100644 index 000000000..327eeda55 --- /dev/null +++ b/app/models/util/report_type/accessors.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ReportType + module Accessors # :nodoc: + def report_type + type = self['report_type'] + type.is_a?(String) ? ReportType[type] : type + end + + def report_type=(type) + type = type.key if type.is_a? ReportType + self['report_type'] = type + end + end +end diff --git a/app/models/util/report_type/auto_start_type.rb b/app/models/util/report_type/auto_start_type.rb new file mode 100644 index 000000000..c92402f5c --- /dev/null +++ b/app/models/util/report_type/auto_start_type.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class ReportType + class AutoStartType < ReportType::StartStopType + INSTANCE = new 'auto_start', 'Von/Bis offen', 12 + + def time_string(worktime) + return unless worktime.from_start_time.is_a?(Time) + + "Start um #{I18n.l(worktime.from_start_time, format: :time)}" + end + + def validate_worktime(worktime) + # set defaults + worktime.work_date = Time.zone.today + worktime.hours = 0 + worktime.to_end_time = nil + # validate + worktime.errors.add(:from_start_time, 'Die Anfangszeit ist ungültig') unless worktime.from_start_time.is_a?(Time) + return unless worktime.employee + + existing = worktime.employee.send(:"running_#{worktime.class.name[0..-5].downcase}") + return unless existing && existing != worktime + + worktime.errors.add(:employee_id, "Es wurde bereits eine offene #{worktime.class.model_name.human} erfasst") + end + end +end diff --git a/app/models/util/report_type/hours_day_type.rb b/app/models/util/report_type/hours_day_type.rb new file mode 100644 index 000000000..2ccbc6d87 --- /dev/null +++ b/app/models/util/report_type/hours_day_type.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ReportType + class HoursDayType < ReportType + INSTANCE = new 'absolute_day', 'Stunden/Tag', 6 + + def time_string(worktime) + "#{rounded_hours(worktime)} h" + end + end +end diff --git a/app/models/util/report_type/hours_month_type.rb b/app/models/util/report_type/hours_month_type.rb new file mode 100644 index 000000000..9c21c4bea --- /dev/null +++ b/app/models/util/report_type/hours_month_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ReportType + class HoursMonthType < ReportType + INSTANCE = new 'month', 'Stunden/Monat', 2 + + def time_string(worktime) + "#{rounded_hours(worktime)} h in diesem Monat" + end + + def date_string(date) + I18n.l(date, format: '%m.%Y') + end + end +end diff --git a/app/models/util/report_type/hours_week_type.rb b/app/models/util/report_type/hours_week_type.rb new file mode 100644 index 000000000..bd3814239 --- /dev/null +++ b/app/models/util/report_type/hours_week_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ReportType + class HoursWeekType < ReportType + INSTANCE = new 'week', 'Stunden/Woche', 4 + + def time_string(worktime) + "#{rounded_hours(worktime)} h in dieser Woche" + end + + def date_string(date) + I18n.l(date, format: 'W %V, %Y') + end + end +end diff --git a/app/models/util/report_type/start_stop_type.rb b/app/models/util/report_type/start_stop_type.rb new file mode 100644 index 000000000..7e95e6a16 --- /dev/null +++ b/app/models/util/report_type/start_stop_type.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class ReportType + class StartStopType < ReportType + INSTANCE = new 'start_stop_day', 'Von/Bis Zeit', 10 + START_STOP = true + + def time_string(worktime) + if worktime.from_start_time.is_a?(Time) && + worktime.to_end_time.is_a?(Time) + "#{I18n.l(worktime.from_start_time, + format: :time)} - #{I18n.l(worktime.to_end_time, format: :time)} (#{rounded_hours(worktime)} h)" + end + end + + def copy_times(source, target) + super(source, target) + target.from_start_time = source.from_start_time + target.to_end_time = source.to_end_time + end + + def validate_worktime(worktime) + worktime.errors.add(:from_start_time, 'Die Anfangszeit ist ungültig') unless worktime.from_start_time.is_a?(Time) + worktime.errors.add(:to_end_time, 'Die Endzeit ist ungültig') unless worktime.to_end_time.is_a?(Time) + if worktime.from_start_time.is_a?(Time) && worktime.to_end_time.is_a?(Time) && + worktime.to_end_time <= worktime.from_start_time + worktime.errors.add(:to_end_time, 'Die Endzeit muss nach der Startzeit sein') + end + return unless worktime.from_start_time&.to_date != worktime.to_end_time&.to_date + + worktime.errors.add(:to_end_time, 'Die Endzeit muss zwischen 00:00-23:59 liegen') + end + end +end diff --git a/app/models/util/week.rb b/app/models/util/week.rb index c4e7ef4ad..3ea99c27a 100644 --- a/app/models/util/week.rb +++ b/app/models/util/week.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -35,7 +37,7 @@ def initialize(year, week) end def to_integer - @year * 100 + @week + (@year * 100) + @week end def valid? diff --git a/app/models/work_item.rb b/app/models/work_item.rb index 1c8930b1e..b8204aef5 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -19,7 +21,7 @@ # closed :boolean default(FALSE), not null # -class WorkItem < ActiveRecord::Base +class WorkItem < ApplicationRecord include Evaluatable ### ASSOCIATIONS @@ -32,19 +34,19 @@ class WorkItem < ActiveRecord::Base has_many :plannings, lambda { |work_item| - joins(:work_item). - unscope(where: :work_item_id). - where('plannings.work_item_id = work_items.id AND ' \ - '? = ANY (work_items.path_ids)', work_item.id) + joins(:work_item) + .unscope(where: :work_item_id) + .where('plannings.work_item_id = work_items.id AND ' \ + '? = ANY (work_items.path_ids)', work_item.id) }, dependent: :destroy has_many :worktimes, lambda { |work_item| - joins(:work_item). - unscope(where: :work_item_id). - where('worktimes.work_item_id = work_items.id AND ' \ - '? = ANY (work_items.path_ids)', work_item.id) + joins(:work_item) + .unscope(where: :work_item_id) + .where('worktimes.work_item_id = work_items.id AND ' \ + '? = ANY (work_items.path_ids)', work_item.id) } ### VALIDATIONS @@ -57,11 +59,11 @@ class WorkItem < ActiveRecord::Base before_validation :upcase_shortname before_save :remember_name_changes - before_update :generate_path_ids after_create :generate_path_ids! after_create :reset_parent_leaf - after_save :update_child_path_names, if: -> { @names_changed } + before_update :generate_path_ids after_destroy :reset_parent_leaf + after_save :update_child_path_names, if: -> { @names_changed } ### SCOPES @@ -92,7 +94,9 @@ def ancestor?(work_item_id) end def label_ancestry - path_names.split("\n")[1..-1].join(" #{Settings.work_items.path_separator} ") + path_names + .split("\n")[1..] + .join(" #{Settings.work_items.path_separator} ") end def top_item @@ -111,11 +115,11 @@ def open? !closed end - def with_ancestors(&block) + def with_ancestors(&) return enum_for(:with_ancestors) unless block_given? yield self - parent.with_ancestors(&block) if parent_id? + parent.with_ancestors(&) if parent_id? end def self_and_descendants @@ -124,21 +128,21 @@ def self_and_descendants # children that are not assigned to a special entity like client or order def categories - children. - joins('LEFT JOIN clients ON clients.work_item_id = work_items.id'). - joins('LEFT JOIN orders ON orders.work_item_id = work_items.id'). - joins('LEFT JOIN accounting_posts ON accounting_posts.work_item_id = work_items.id'). - where(clients: { id: nil }, - orders: { id: nil }, - accounting_posts: { id: nil }) + children + .joins('LEFT JOIN clients ON clients.work_item_id = work_items.id') + .joins('LEFT JOIN orders ON orders.work_item_id = work_items.id') + .joins('LEFT JOIN accounting_posts ON accounting_posts.work_item_id = work_items.id') + .where(clients: { id: nil }, + orders: { id: nil }, + accounting_posts: { id: nil }) end def employees - Employee. - where('id IN (?) OR id IN (?)', - plannings.select(:employee_id), - worktimes.select(:employee_id)). - list + Employee + .where('id IN (?) OR id IN (?)', + plannings.select(:employee_id), + worktimes.select(:employee_id)) + .list end def move_times!(target) @@ -156,7 +160,7 @@ def update_path_names! end def propagate_closed!(closed) - self_and_descendants.update_all(closed: closed) + self_and_descendants.update_all(closed:) self.closed = closed save! end @@ -179,9 +183,9 @@ def generate_path_ids! end def reset_parent_leaf - if parent - parent.update_column(:leaf, !parent.children.exists?) - end + return unless parent + + parent.update_column(:leaf, !parent.children.exists?) end def update_child_path_names @@ -196,6 +200,6 @@ def store_path_names end def upcase_shortname - shortname.upcase! if shortname + shortname&.upcase! end end diff --git a/app/models/working_condition.rb b/app/models/working_condition.rb index 54355673a..6c50eb4d6 100644 --- a/app/models/working_condition.rb +++ b/app/models/working_condition.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -13,7 +15,7 @@ # must_hours_per_day :decimal(4, 2) not null # -class WorkingCondition < ActiveRecord::Base +class WorkingCondition < ApplicationRecord validates_by_schema validates :valid_from, uniqueness: true validates :must_hours_per_day, @@ -23,12 +25,12 @@ class WorkingCondition < ActiveRecord::Base validate :exactly_one_without_valid_from before_destroy :protect_blank_valid_from - after_save :clear_cache after_destroy :clear_cache + after_save :clear_cache delegate :clear_cache, to: :class - scope :list, -> { order('(CASE WHEN valid_from IS NULL THEN 0 ELSE 1 END) DESC, valid_from DESC') } + scope :list, -> { order(Arel.sql('(CASE WHEN valid_from IS NULL THEN 0 ELSE 1 END) DESC, valid_from DESC')) } class << self def todays_value(attr) @@ -78,13 +80,13 @@ def cached # double cache for best performance RequestStore.store[model_name.route_key] ||= Rails.cache.fetch(model_name.route_key) do - order('(CASE WHEN valid_from IS NULL THEN 0 ELSE 1 END), valid_from').collect(&:attributes) + order(Arel.sql('(CASE WHEN valid_from IS NULL THEN 0 ELSE 1 END), valid_from')).collect(&:attributes) end end def clear_cache RequestStore.store[model_name.route_key] = nil - Rails.cache.clear(model_name.route_key) + Rails.cache.delete(model_name.route_key) @todays_values = {} true end @@ -97,16 +99,16 @@ def to_s private def exactly_one_without_valid_from - first_id = WorkingCondition.where(valid_from: nil).pluck(:id).first - if id == first_id && valid_from? - errors.add(:valid_from, 'darf für den ersten Eintrag nicht gesetzt werden.') - end + first_id = WorkingCondition.where(valid_from: nil).pick(:id) + return unless id == first_id && valid_from? + + errors.add(:valid_from, 'darf für den ersten Eintrag nicht gesetzt werden.') end def protect_blank_valid_from - if valid_from.blank? - errors.add(:base, 'Der erste Eintrag darf nicht gelöscht werden.') - throw :abort - end + return if valid_from.present? + + errors.add(:base, 'Der erste Eintrag darf nicht gelöscht werden.') + throw :abort end end diff --git a/app/models/workplace.rb b/app/models/workplace.rb new file mode 100644 index 000000000..26cfd7277 --- /dev/null +++ b/app/models/workplace.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Copyright (c) 2006-2022, Puzzle ITC GmbH. This file is part of +# PuzzleTime and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/puzzle/puzzletime. + +class Workplace < ApplicationRecord + validates_by_schema + validates :name, uniqueness: { case_sensitive: false } + + scope :list, -> { order(:name) } + + def to_s + name + end +end diff --git a/app/models/worktime.rb b/app/models/worktime.rb index b748921cd..59a4f403d 100644 --- a/app/models/worktime.rb +++ b/app/models/worktime.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -23,7 +25,7 @@ # invoice_id :integer # -class Worktime < ActiveRecord::Base +class Worktime < ApplicationRecord H_M = /^(\d*):([0-5]\d)/ include ReportType::Accessors @@ -37,7 +39,6 @@ class Worktime < ActiveRecord::Base belongs_to :invoice, optional: true validates_by_schema - validates :employee_id, presence: true validates :work_date, timeliness: { date: true } validate :validate_by_report_type @@ -76,19 +77,17 @@ def account end # account id, default nil - def account_id - end + def account_id; end # sets the account id. # overwrite in subclass - def account_id=(_value) - end + def account_id=(_value); end # set the hours, either as number or as a string with the format # h:mm or h.dd (8:45 <-> 8.75) def hours=(value) - if md = H_M.match(value.to_s) - value = md[1].to_i + md[2].to_i / 60.0 + if (md = H_M.match(value.to_s)) + value = md[1].to_i + (md[2].to_i / 60.0) end self['hours'] = value.to_f end @@ -107,7 +106,7 @@ def to_end_time=(value) # Returns a human readable String of the time information contained in this Worktime. def time_string - report_type.time_string(self) if report_type + report_type&.time_string(self) end # Returns the date formatted according to the report type @@ -124,7 +123,7 @@ def absence? # Whether the report typ of this Worktime contains start and stop times def start_stop? - report_type.start_stop? if report_type + report_type&.start_stop? end ################## HELPERS #################### @@ -133,7 +132,7 @@ def start_stop? def template(new_worktime = nil) new_worktime ||= self.class.new new_worktime.from_start_time = - if report_type.is_a?(StartStopType) + if report_type.is_a?(ReportType::StartStopType) to_end_time else Time.zone.now.change(hour: Settings.defaults.start_hour) @@ -164,16 +163,16 @@ def copy_times_from(other) # Validate callback before saving def validate_by_report_type - report_type.validate_worktime(self) if report_type + report_type&.validate_worktime(self) end def guess_report_type if from_start_time || to_end_time - self.report_type = StartStopType::INSTANCE + self.report_type = ReportType::StartStopType::INSTANCE else self.from_start_time = nil self.to_end_time = nil - self.report_type = HoursDayType::INSTANCE + self.report_type = ReportType::HoursDayType::INSTANCE end end @@ -187,7 +186,7 @@ def store_hours self.hours = nil end end - self.work_date = Time.zone.today if report_type.is_a? AutoStartType + self.work_date = Time.zone.today if report_type.is_a? ReportType::AutoStartType end def strip_ticket @@ -200,7 +199,8 @@ def controller end def to_s - "#{time_string} #{self.class.model_name.human} #{'für ' + account.label_verbose if account}" + account_part = "für #{account.label_verbose}" if account + "#{time_string} #{self.class.model_name.human} #{account_part}" end ####################### CLASS METHODS FOR EVALUATABLE #################### @@ -225,11 +225,11 @@ def write_converted_time(attribute, value) if value.is_a?(String) && value !~ H_M if !value.empty? && value =~ /^\d*\.?\d*$/ # military time: 1400 - if value.size > 2 && !value.include?('.') + if value.size > 2 && value.exclude?('.') hour = value.to_i / 100 - value = hour.to_s + ':' + (value.to_i - hour * 100).to_s + value = "#{hour}:#{value.to_i - (hour * 100)}" else - value = value.to_i.to_s + ':' + ((value.to_f - value.to_i) * 60).to_i.to_s + value = "#{value.to_i}:#{((value.to_f - value.to_i) * 60).to_i}" end else value = nil diff --git a/app/presenters/employees/vcard.rb b/app/presenters/employees/vcard.rb new file mode 100644 index 000000000..5db4b227c --- /dev/null +++ b/app/presenters/employees/vcard.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Employees + class Vcard + TEMPLATE_FILE = File.expand_path('vcard.vcf.haml', __dir__) + + attr_reader :employee, :include + + def initialize(employee, include: nil) + @employee = employee + @include = include + end + + def render + Haml::Template.new(TEMPLATE_FILE).render(nil, employee: self) + end + + def method_missing(method_name, *) + return employee.send(method_name, *) if include.blank? || include.include?(method_name) + + nil + end + + def respond_to_missing?(method_name, include_private = false) + return employee.respond_to?(method_name, include_private) + super + end + + private + + def template + @template ||= File.read(TEMPLATE_FILE) + end + end +end diff --git a/app/presenters/employees/vcard.vcf.haml b/app/presenters/employees/vcard.vcf.haml new file mode 100644 index 000000000..8f3f7a66d --- /dev/null +++ b/app/presenters/employees/vcard.vcf.haml @@ -0,0 +1,19 @@ +-# Copyright (c) 2006-2023, Puzzle ITC GmbH. This file is part of +-# PuzzleTime and licensed under the Affero General Public License version 3 +-# or later. See the COPYING file at the top-level directory or at +-# https://github.com/puzzle/puzzletime. + +BEGIN:VCARD +VERSION:3.0 +N:#{employee.lastname};#{employee.firstname};;; +FN:#{[employee.firstname, employee.lastname].compact.join(' ')} +- if employee.street.present? || employee.city.present? || employee.postal_code.present? + ADR;TYPE=HOME,PREF:;;#{employee.street};#{employee.city};;#{employee.postal_code}; +- if employee.phone_office.present? + TEL;TYPE=WORK,VOICE:#{employee.phone_office} +- if employee.phone_private.present? + TEL;TYPE=CELL,PREF,VOICE:#{employee.phone_private} +EMAIL;TYPE=WORK,PREF:#{employee.email} +- if employee.birthday.present? + BDAY:#{employee.birthday.strftime('%Y%m%d')} +END:VCARD diff --git a/app/validators/email_validator.rb b/app/validators/email_validator.rb new file mode 100644 index 000000000..42666e768 --- /dev/null +++ b/app/validators/email_validator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class EmailValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return if EmailAddress.valid?(value, host_validation: :syntax) + + record.errors.add attribute, (options[:message] || I18n.t('error.message.invalid_email')) + end +end diff --git a/app/views/clients/categories.json.jbuilder b/app/views/clients/categories.json.jbuilder index f6d6f0a74..bcceb845d 100644 --- a/app/views/clients/categories.json.jbuilder +++ b/app/views/clients/categories.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.array!(@categories) do |entry| json.extract! entry, :id, :name, :shortname json.label entry.name diff --git a/app/views/configurations/index.html.haml b/app/views/configurations/index.html.haml index d4c70acc5..6e345a820 100644 --- a/app/views/configurations/index.html.haml +++ b/app/views/configurations/index.html.haml @@ -12,3 +12,6 @@ = render 'group', title: 'Aufträge', models: [OrderKind, OrderStatus, TargetScope] = render 'group', title: 'Funktionen', models: [EmploymentRole, EmploymentRoleLevel, EmploymentRoleCategory] + +.row + = render 'group', title: 'Stammdaten', models: [Workplace] diff --git a/app/views/contacts/with_crm.json.jbuilder b/app/views/contacts/with_crm.json.jbuilder index 1cb48452b..c287685d2 100644 --- a/app/views/contacts/with_crm.json.jbuilder +++ b/app/views/contacts/with_crm.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.array! @entries do |contact| json.id_or_crm contact.id_or_crm json.label contact.to_s diff --git a/app/views/crud/show.json.jbuilder b/app/views/crud/show.json.jbuilder index 7feb40546..5bad23571 100644 --- a/app/views/crud/show.json.jbuilder +++ b/app/views/crud/show.json.jbuilder @@ -1 +1,3 @@ +# frozen_string_literal: true + json.extract! entry, :id, *default_crud_attrs diff --git a/app/views/devise/shared/_links.html.haml b/app/views/devise/shared/_links.html.haml index e986cb90b..f8f1a53b8 100644 --- a/app/views/devise/shared/_links.html.haml +++ b/app/views/devise/shared/_links.html.haml @@ -21,9 +21,16 @@ - if devise_mapping.omniauthable? - resource_class.omniauth_providers.each do |provider| - if Settings.auth&.omniauth[provider]&.active - = link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", + - provider_label = Settings.auth.omniauth.public_send(provider).label || OmniAuth::Utils.camelize(provider) + = link_to "Mit #{provider_label} anmelden", public_send("#{resource_name}_#{provider}_omniauth_authorize_path"), method: :post, - class: 'btn btn-primary' + class: try(:auto_redirect?) ? 'btn btn-primary auto-login' : 'btn btn-primary' %br %br + +- if try(:auto_redirect?) + :coffeescript + $(document).on('turbolinks:load', -> + $('.auto-login').click(); + ) \ No newline at end of file diff --git a/app/views/employee_mailer/worktime_commit_reminder_mail.html.haml b/app/views/employee_mailer/worktime_commit_reminder_mail.html.haml new file mode 100644 index 000000000..2e9168b62 --- /dev/null +++ b/app/views/employee_mailer/worktime_commit_reminder_mail.html.haml @@ -0,0 +1,19 @@ +%h1.h3{:style => "box-sizing: border-box; margin: 0.67em 0; font-family: inherit; font-weight: 400; line-height: 1.1; color: inherit; margin-top: 20px; font-size: 24px; margin-bottom: 20px;"} + Hallo + = @employee.firstname +%div{:style => "box-sizing: border-box;"} + .lead + Es hat wieder ein neuer Monat begonnen. + %br + Bitte gib im + = link_to 'PuzzleTime', root_url + noch deine Zeiten frei, Danke! + %br + .lead + Liebe Grüsse + %br + Dein PuzzleTime + %br + Falls du keine Erinnerungsmails wünschst, kannst du sie + = link_to 'hier', settings_employees_url + abschalten. diff --git a/app/views/employee_mailer/worktime_commit_reminder_mail.text.erb b/app/views/employee_mailer/worktime_commit_reminder_mail.text.erb new file mode 100644 index 000000000..0558067c5 --- /dev/null +++ b/app/views/employee_mailer/worktime_commit_reminder_mail.text.erb @@ -0,0 +1,9 @@ +Hallo <%= @employee.firstname %> + +Es hat wieder ein neuer Monat begonnen. +Bitte gib im PuzzleTime noch deine Zeiten frei, Danke! + +Falls du keine Erinnerungsmails wünschst, kannst du sie hier abschalten: <%= settings_employees_url %> + +Liebe Grüsse +Dein PuzzleTime diff --git a/app/views/employee_mailer/worktime_deleted_mail.html.haml b/app/views/employee_mailer/worktime_deleted_mail.html.haml new file mode 100644 index 000000000..3176c1433 --- /dev/null +++ b/app/views/employee_mailer/worktime_deleted_mail.html.haml @@ -0,0 +1,15 @@ +%h1.h3{:style => "box-sizing: border-box; margin: 0.67em 0; font-family: inherit; font-weight: 400; line-height: 1.1; color: inherit; margin-top: 20px; font-size: 24px; margin-bottom: 20px;"} + Hallo + = @worktime.employee.firstname +%div{:style => "box-sizing: border-box;"} + .lead + = @deleted_by + hat deinen PuzzleTime-Eintrag gelöscht: + %br + = f(@worktime.work_date) + = @worktime + %br + .text-center + Liebe Grüsse + %br + Dein PuzzleTime diff --git a/app/views/employee_mailer/worktime_deleted_mail.text.erb b/app/views/employee_mailer/worktime_deleted_mail.text.erb index 99507d483..dd2cdebae 100644 --- a/app/views/employee_mailer/worktime_deleted_mail.text.erb +++ b/app/views/employee_mailer/worktime_deleted_mail.text.erb @@ -1,4 +1,4 @@ -Hallo <%= @worktime.employee %> +Hallo <%= @worktime.employee.firstname %> <%= @deleted_by %> hat deinen PuzzleTime-Eintrag gelöscht: <%= f(@worktime.work_date) %> <%= @worktime %> diff --git a/app/views/employee_master_data/_attrs.html.haml b/app/views/employee_master_data/_attrs.html.haml index 23e94c41f..e11a9a71a 100644 --- a/app/views/employee_master_data/_attrs.html.haml +++ b/app/views/employee_master_data/_attrs.html.haml @@ -32,8 +32,9 @@ %i.glyphicon.glyphicon-envelope = link_to @employee.email, "mailto:#{@employee.email}" .col-md-6 + = link_to({format: :png}, target: :_blank) do + = image_tag('qr-code-scan-icon.svg', class: 'qr-code master-data-vcard-qrcode') = link_to format: :vcf do - %img.master-data-vcard-qrcode{ class: 'qr-code', src: employee_master_datum_url(id: @employee.id, format: :svg) } vCard herunterladen .row @@ -76,6 +77,7 @@ %h4 Anstellung = labeled_attr(@employee, :department_id) = labeled_attr(@employee, :current_percent) + = labeled_attr(@employee, :workplace_id) = labeled('Rollen', format_current_employment_roles(@employee, tag(:br))) - if @employee.additional_information.present? diff --git a/app/views/employee_master_data/show.vcf.haml b/app/views/employee_master_data/show.vcf.haml deleted file mode 100644 index ccb3182d2..000000000 --- a/app/views/employee_master_data/show.vcf.haml +++ /dev/null @@ -1,20 +0,0 @@ --# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of --# PuzzleTime and licensed under the Affero General Public License version 3 --# or later. See the COPYING file at the top-level directory or at --# https://github.com/puzzle/puzzletime. - - -BEGIN:VCARD -VERSION:3.0 -N:#{@employee.lastname};#{@employee.firstname};;; -FN:#{@employee.to_s} -- if @employee.street.present? || @employee.city.present? || @employee.postal_code.present? - ADR;TYPE=HOME,PREF:;;#{@employee.street};#{@employee.city};;#{@employee.postal_code}; -- if @employee.phone_office.present? - TEL;TYPE=WORK,VOICE:#{@employee.phone_office} -- if @employee.phone_private.present? - TEL;TYPE=CELL,PREF,VOICE:#{@employee.phone_private} -EMAIL;TYPE=WORK,PREF:#{@employee.email} -- if @employee.birthday.present? - BDAY:#{@employee.birthday.strftime('%Y%m%d')} -END:VCARD diff --git a/app/views/employees/_form.html.haml b/app/views/employees/_form.html.haml index 7b1390263..cdfe6c1d7 100644 --- a/app/views/employees/_form.html.haml +++ b/app/views/employees/_form.html.haml @@ -11,6 +11,7 @@ :email, :ldapname, :department_id, + :workplace_id, :probation_period_end_date, :graduation diff --git a/app/views/employees/passwd.html.haml b/app/views/employees/passwd.html.haml deleted file mode 100644 index 2e86cc647..000000000 --- a/app/views/employees/passwd.html.haml +++ /dev/null @@ -1,29 +0,0 @@ --# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of --# PuzzleTime and licensed under the Affero General Public License version 3 --# or later. See the COPYING file at the top-level directory or at --# https://github.com/puzzle/puzzletime. - - -- @title = 'Passwort ändern' - -= form_tag passwd_employees_path, name: 'form' do - %table.table-condensed - %tr - %td - %label.control-label Altes Passwort - %td= password_field_tag 'pwd', nil, size: 30, class: 'form-control initial-focus' - %tr - %td - %label.control-label Neues Passwort - %td= password_field_tag 'change_pwd', nil, size: 30, class: 'form-control' - %tr - %td - %label.control-label Bestätigung - %td= password_field_tag 'change_pwd_confirmation', nil, size: 30, class: 'form-control' - %tr - %td - %td - %input{type: "submit", value: "Speichern", class: 'btn btn-primary'} - = link_to('Abbrechen', settings_employees_path, class: 'cancel') - - diff --git a/app/views/employees/settings.html.haml b/app/views/employees/settings.html.haml index a107a7469..8f5a4bc4f 100644 --- a/app/views/employees/settings.html.haml +++ b/app/views/employees/settings.html.haml @@ -12,44 +12,46 @@ %p.form-control-static = link_to 'Ändern', edit_employee_registration_path - = f.labeled('Zeitspannen') do + = f.labeled_input_field :worktimes_commit_reminder + + = f.labeled(:eval_periods) do %table.table-condensed %tr{:valign => "top"} %td %label - = multiple_check_box 'user', 'eval_periods', '0d' + = multiple_check_box 'employee', 'eval_periods', '0d' Heute %br/ %label - = multiple_check_box 'user', 'eval_periods', '-1d' + = multiple_check_box 'employee', 'eval_periods', '-1d' Gestern %td %label - = multiple_check_box 'user', 'eval_periods', '0w' + = multiple_check_box 'employee', 'eval_periods', '0w' Aktuelle Woche %br/ %label - = multiple_check_box 'user', 'eval_periods', '-1w' + = multiple_check_box 'employee', 'eval_periods', '-1w' Vorige Woche %td %label - = multiple_check_box 'user', 'eval_periods', '0m' + = multiple_check_box 'employee', 'eval_periods', '0m' Aktueller Monat %br/ %label - = multiple_check_box 'user', 'eval_periods', '-1m' + = multiple_check_box 'employee', 'eval_periods', '-1m' Voriger Monat %td %label - = multiple_check_box 'user', 'eval_periods', '0y' + = multiple_check_box 'employee', 'eval_periods', '0y' Aktuelles Jahr %br/ %label - = multiple_check_box 'user', 'eval_periods', '-1y' + = multiple_check_box 'employee', 'eval_periods', '-1y' Voriges Jahr %td %label - = multiple_check_box 'user', 'eval_periods', '0' + = multiple_check_box 'employee', 'eval_periods', '0' Total - unless @user.overtime_vacations.empty? diff --git a/app/views/evaluator/_division.html.haml b/app/views/evaluator/_division.html.haml index 4e56560ae..d7fcea1fc 100644 --- a/app/views/evaluator/_division.html.haml +++ b/app/views/evaluator/_division.html.haml @@ -9,7 +9,7 @@ %td - if sub = evaluation.sub_evaluation = link_to(division.label_verbose, - { action: 'overview', evaluation: sub, category_id: division.id }, + { action: 'overview', evaluation: sub, category_id: division.id, **search_conditions }, title: division.tooltip) - else = content_tag :span, division.label_verbose, title: division.tooltip diff --git a/app/views/evaluator/_employee_absences.html.haml b/app/views/evaluator/_employee_absences.html.haml index 8f3d11b87..8c8dce7f5 100644 --- a/app/views/evaluator/_employee_absences.html.haml +++ b/app/views/evaluator/_employee_absences.html.haml @@ -7,7 +7,7 @@ %section.employee-absences %h4 Absenzen - - evaluation = EmployeeAbsencesEval.new(params[:category_id] || @user.id) + - evaluation = Evaluations::EmployeeAbsencesEval.new(params[:category_id] || @user.id, **search_conditions) = render('category', evaluation: evaluation, times: @periods.collect { |p| evaluation.sum_times_grouped(p) }, diff --git a/app/views/evaluator/_evallink.html.haml b/app/views/evaluator/_evallink.html.haml index 40704dc50..6e9d44237 100644 --- a/app/views/evaluator/_evallink.html.haml +++ b/app/views/evaluator/_evallink.html.haml @@ -9,4 +9,5 @@ evallink[2], action: 'overview', evaluation: evallink[0], - category_id: evallink[1] + category_id: evallink[1], + **search_conditions diff --git a/app/views/evaluator/_timecell.html.haml b/app/views/evaluator/_timecell.html.haml index c552693a2..f0236f48b 100644 --- a/app/views/evaluator/_timecell.html.haml +++ b/app/views/evaluator/_timecell.html.haml @@ -37,7 +37,8 @@ action: action, evaluation: evaluation_name, category_id: evaluation.category_id, - division_id: division_id.presence) + division_id: division_id.presence, + absence_id: params[:absence_id]) - link += '&' + ( p ? p.url_query_s : 'start_date=0' ) -# use html link instead of link_to for performance reasons %a{ href: link.html_safe }= image_tag('lupe.gif', size: '19x11', alt: 'Details') diff --git a/app/views/evaluator/details.html.haml b/app/views/evaluator/details.html.haml index bf87c12d5..bbf81449a 100644 --- a/app/views/evaluator/details.html.haml +++ b/app/views/evaluator/details.html.haml @@ -9,6 +9,11 @@ %h3= @evaluation.category_label %h3= @evaluation.division_label +- if @absence + %h3 + Absenztyp: + = @absence + = render 'detail_times' %p diff --git a/app/views/evaluator/overview.html.haml b/app/views/evaluator/overview.html.haml index 404fc1a32..d9ed7aa67 100644 --- a/app/views/evaluator/overview.html.haml +++ b/app/views/evaluator/overview.html.haml @@ -9,6 +9,13 @@ - if @order = "#{@order.model_name.human}: #{link_to(@order.label_with_workitem_path, @order)}".html_safe +- if evaluation_type =~ /absence/ + = form_tag(nil, { method: :get, class: 'form-inline', role: 'search' }) do + = hidden_field_tag :returning, true + = hidden_field_tag :page, 1 + = hidden_field_tag :category_id, params[:category_id] + = direct_filter_select(:absence_id, 'Absenztyp', Absence.all) + = render 'period_link' - if @evaluation.category.respond_to?(:tooltip) && @evaluation.category.tooltip.present? diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml new file mode 100644 index 000000000..43e907029 --- /dev/null +++ b/app/views/layouts/mailer.html.haml @@ -0,0 +1,1464 @@ +%div{ style: "border-bottom: 1px solid #D8D8D8; height: 40px; padding-top: 3px; padidng-bottom: 10px; font-size: 22px; font-weight: 400; font-family: 'sans-serif'" } + %div{ style: "inline-block" } + + + +!!! +%html.csspositionsticky{:lang => "de", :style => "box-sizing: border-box; font-family: sans-serif; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; font-size: 10px; -webkit-tap-highlight-color: rgba(0,0,0,0);"} + %head + %meta{:charset => "utf-8"}/ + %title + PuzzleTime + %meta{:content => "width=device-width, initial-scale=1.0", :name => "viewport"}/ + :css + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmSU5fCRc4EsA.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmSU5fABc4EsA.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmSU5fCBc4EsA.woff2) format('woff2'); + unicode-range: U+1F00-1FFF;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmSU5fBxc4EsA.woff2) format('woff2'); + unicode-range: U+0370-03FF;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmSU5fCxc4EsA.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmSU5fChc4EsA.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmSU5fBBc4.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOmCnqEu92Fr1Mu72xKOzY.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOmCnqEu92Fr1Mu5mxKOzY.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOmCnqEu92Fr1Mu7mxKOzY.woff2) format('woff2'); + unicode-range: U+1F00-1FFF;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOmCnqEu92Fr1Mu4WxKOzY.woff2) format('woff2'); + unicode-range: U+0370-03FF;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOmCnqEu92Fr1Mu7WxKOzY.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOmCnqEu92Fr1Mu7GxKOzY.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOmCnqEu92Fr1Mu4mxK.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmEU9fCRc4EsA.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmEU9fABc4EsA.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmEU9fCBc4EsA.woff2) format('woff2'); + unicode-range: U+1F00-1FFF;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmEU9fBxc4EsA.woff2) format('woff2'); + unicode-range: U+0370-03FF;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmEU9fCxc4EsA.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;} + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmEU9fBBc4.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;} + @media print { + *, + *:before, + *:after { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + box-shadow: none !important; + } + + a, + a:visited { + text-decoration: underline; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h2, + h3 { + page-break-after: avoid; + } + + .navbar { + display: none; + } + + .btn>.caret, + .dropup>.btn>.caret { + border-top-color: #000 !important; + } + + .label { + border: 1px solid #000; + } + + .table { + border-collapse: collapse !important; + } + + .table td, + .table th { + background-color: #fff !important; + } + + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } + } + + @media (min-width: 768px) { + .lead { + font-size: 21px; + } + } + @media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + clear: left; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .dl-horizontal dd { + margin-left: 180px; + } + } + @media (min-width: 768px) { + .container { + width: 750px; + } + } + @media (min-width: 992px) { + .container { + width: 970px; + } + } + @media (min-width: 1200px) { + .container { + width: 1170px; + } + } + @media (min-width: 768px) { + .col-sm-1, + .col-sm-2, + .col-sm-3, + .col-sm-4, + .col-sm-5, + .col-sm-6, + .col-sm-7, + .col-sm-8, + .col-sm-9, + .col-sm-10, + .col-sm-11, + .col-sm-12 { + float: left; + } + + .col-sm-1 { + width: 8.3333333333%; + } + + .col-sm-2 { + width: 16.6666666667%; + } + + .col-sm-3 { + width: 25%; + } + + .col-sm-4 { + width: 33.3333333333%; + } + + .col-sm-5 { + width: 41.6666666667%; + } + + .col-sm-6 { + width: 50%; + } + + .col-sm-7 { + width: 58.3333333333%; + } + + .col-sm-8 { + width: 66.6666666667%; + } + + .col-sm-9 { + width: 75%; + } + + .col-sm-10 { + width: 83.3333333333%; + } + + .col-sm-11 { + width: 91.6666666667%; + } + + .col-sm-12 { + width: 100%; + } + + .col-sm-pull-0 { + right: auto; + } + + .col-sm-pull-1 { + right: 8.3333333333%; + } + + .col-sm-pull-2 { + right: 16.6666666667%; + } + + .col-sm-pull-3 { + right: 25%; + } + + .col-sm-pull-4 { + right: 33.3333333333%; + } + + .col-sm-pull-5 { + right: 41.6666666667%; + } + + .col-sm-pull-6 { + right: 50%; + } + + .col-sm-pull-7 { + right: 58.3333333333%; + } + + .col-sm-pull-8 { + right: 66.6666666667%; + } + + .col-sm-pull-9 { + right: 75%; + } + + .col-sm-pull-10 { + right: 83.3333333333%; + } + + .col-sm-pull-11 { + right: 91.6666666667%; + } + + .col-sm-pull-12 { + right: 100%; + } + + .col-sm-push-0 { + left: auto; + } + + .col-sm-push-1 { + left: 8.3333333333%; + } + + .col-sm-push-2 { + left: 16.6666666667%; + } + + .col-sm-push-3 { + left: 25%; + } + + .col-sm-push-4 { + left: 33.3333333333%; + } + + .col-sm-push-5 { + left: 41.6666666667%; + } + + .col-sm-push-6 { + left: 50%; + } + + .col-sm-push-7 { + left: 58.3333333333%; + } + + .col-sm-push-8 { + left: 66.6666666667%; + } + + .col-sm-push-9 { + left: 75%; + } + + .col-sm-push-10 { + left: 83.3333333333%; + } + + .col-sm-push-11 { + left: 91.6666666667%; + } + + .col-sm-push-12 { + left: 100%; + } + + .col-sm-offset-0 { + margin-left: 0%; + } + + .col-sm-offset-1 { + margin-left: 8.3333333333%; + } + + .col-sm-offset-2 { + margin-left: 16.6666666667%; + } + + .col-sm-offset-3 { + margin-left: 25%; + } + + .col-sm-offset-4 { + margin-left: 33.3333333333%; + } + + .col-sm-offset-5 { + margin-left: 41.6666666667%; + } + + .col-sm-offset-6 { + margin-left: 50%; + } + + .col-sm-offset-7 { + margin-left: 58.3333333333%; + } + + .col-sm-offset-8 { + margin-left: 66.6666666667%; + } + + .col-sm-offset-9 { + margin-left: 75%; + } + + .col-sm-offset-10 { + margin-left: 83.3333333333%; + } + + .col-sm-offset-11 { + margin-left: 91.6666666667%; + } + + .col-sm-offset-12 { + margin-left: 100%; + } + } + @media (min-width: 992px) { + .col-md-1, + .col-md-2, + .col-md-3, + .col-md-4, + .col-md-5, + .col-md-6, + .col-md-7, + .col-md-8, + .col-md-9, + .col-md-10, + .col-md-11, + .col-md-12 { + float: left; + } + + .col-md-1 { + width: 8.3333333333%; + } + + .col-md-2 { + width: 16.6666666667%; + } + + .col-md-3 { + width: 25%; + } + + .col-md-4 { + width: 33.3333333333%; + } + + .col-md-5 { + width: 41.6666666667%; + } + + .col-md-6 { + width: 50%; + } + + .col-md-7 { + width: 58.3333333333%; + } + + .col-md-8 { + width: 66.6666666667%; + } + + .col-md-9 { + width: 75%; + } + + .col-md-10 { + width: 83.3333333333%; + } + + .col-md-11 { + width: 91.6666666667%; + } + + .col-md-12 { + width: 100%; + } + + .col-md-pull-0 { + right: auto; + } + + .col-md-pull-1 { + right: 8.3333333333%; + } + + .col-md-pull-2 { + right: 16.6666666667%; + } + + .col-md-pull-3 { + right: 25%; + } + + .col-md-pull-4 { + right: 33.3333333333%; + } + + .col-md-pull-5 { + right: 41.6666666667%; + } + + .col-md-pull-6 { + right: 50%; + } + + .col-md-pull-7 { + right: 58.3333333333%; + } + + .col-md-pull-8 { + right: 66.6666666667%; + } + + .col-md-pull-9 { + right: 75%; + } + + .col-md-pull-10 { + right: 83.3333333333%; + } + + .col-md-pull-11 { + right: 91.6666666667%; + } + + .col-md-pull-12 { + right: 100%; + } + + .col-md-push-0 { + left: auto; + } + + .col-md-push-1 { + left: 8.3333333333%; + } + + .col-md-push-2 { + left: 16.6666666667%; + } + + .col-md-push-3 { + left: 25%; + } + + .col-md-push-4 { + left: 33.3333333333%; + } + + .col-md-push-5 { + left: 41.6666666667%; + } + + .col-md-push-6 { + left: 50%; + } + + .col-md-push-7 { + left: 58.3333333333%; + } + + .col-md-push-8 { + left: 66.6666666667%; + } + + .col-md-push-9 { + left: 75%; + } + + .col-md-push-10 { + left: 83.3333333333%; + } + + .col-md-push-11 { + left: 91.6666666667%; + } + + .col-md-push-12 { + left: 100%; + } + + .col-md-offset-0 { + margin-left: 0%; + } + + .col-md-offset-1 { + margin-left: 8.3333333333%; + } + + .col-md-offset-2 { + margin-left: 16.6666666667%; + } + + .col-md-offset-3 { + margin-left: 25%; + } + + .col-md-offset-4 { + margin-left: 33.3333333333%; + } + + .col-md-offset-5 { + margin-left: 41.6666666667%; + } + + .col-md-offset-6 { + margin-left: 50%; + } + + .col-md-offset-7 { + margin-left: 58.3333333333%; + } + + .col-md-offset-8 { + margin-left: 66.6666666667%; + } + + .col-md-offset-9 { + margin-left: 75%; + } + + .col-md-offset-10 { + margin-left: 83.3333333333%; + } + + .col-md-offset-11 { + margin-left: 91.6666666667%; + } + + .col-md-offset-12 { + margin-left: 100%; + } + } + @media (min-width: 1200px) { + .col-lg-1, + .col-lg-2, + .col-lg-3, + .col-lg-4, + .col-lg-5, + .col-lg-6, + .col-lg-7, + .col-lg-8, + .col-lg-9, + .col-lg-10, + .col-lg-11, + .col-lg-12 { + float: left; + } + + .col-lg-1 { + width: 8.3333333333%; + } + + .col-lg-2 { + width: 16.6666666667%; + } + + .col-lg-3 { + width: 25%; + } + + .col-lg-4 { + width: 33.3333333333%; + } + + .col-lg-5 { + width: 41.6666666667%; + } + + .col-lg-6 { + width: 50%; + } + + .col-lg-7 { + width: 58.3333333333%; + } + + .col-lg-8 { + width: 66.6666666667%; + } + + .col-lg-9 { + width: 75%; + } + + .col-lg-10 { + width: 83.3333333333%; + } + + .col-lg-11 { + width: 91.6666666667%; + } + + .col-lg-12 { + width: 100%; + } + + .col-lg-pull-0 { + right: auto; + } + + .col-lg-pull-1 { + right: 8.3333333333%; + } + + .col-lg-pull-2 { + right: 16.6666666667%; + } + + .col-lg-pull-3 { + right: 25%; + } + + .col-lg-pull-4 { + right: 33.3333333333%; + } + + .col-lg-pull-5 { + right: 41.6666666667%; + } + + .col-lg-pull-6 { + right: 50%; + } + + .col-lg-pull-7 { + right: 58.3333333333%; + } + + .col-lg-pull-8 { + right: 66.6666666667%; + } + + .col-lg-pull-9 { + right: 75%; + } + + .col-lg-pull-10 { + right: 83.3333333333%; + } + + .col-lg-pull-11 { + right: 91.6666666667%; + } + + .col-lg-pull-12 { + right: 100%; + } + + .col-lg-push-0 { + left: auto; + } + + .col-lg-push-1 { + left: 8.3333333333%; + } + + .col-lg-push-2 { + left: 16.6666666667%; + } + + .col-lg-push-3 { + left: 25%; + } + + .col-lg-push-4 { + left: 33.3333333333%; + } + + .col-lg-push-5 { + left: 41.6666666667%; + } + + .col-lg-push-6 { + left: 50%; + } + + .col-lg-push-7 { + left: 58.3333333333%; + } + + .col-lg-push-8 { + left: 66.6666666667%; + } + + .col-lg-push-9 { + left: 75%; + } + + .col-lg-push-10 { + left: 83.3333333333%; + } + + .col-lg-push-11 { + left: 91.6666666667%; + } + + .col-lg-push-12 { + left: 100%; + } + + .col-lg-offset-0 { + margin-left: 0%; + } + + .col-lg-offset-1 { + margin-left: 8.3333333333%; + } + + .col-lg-offset-2 { + margin-left: 16.6666666667%; + } + + .col-lg-offset-3 { + margin-left: 25%; + } + + .col-lg-offset-4 { + margin-left: 33.3333333333%; + } + + .col-lg-offset-5 { + margin-left: 41.6666666667%; + } + + .col-lg-offset-6 { + margin-left: 50%; + } + + .col-lg-offset-7 { + margin-left: 58.3333333333%; + } + + .col-lg-offset-8 { + margin-left: 66.6666666667%; + } + + .col-lg-offset-9 { + margin-left: 75%; + } + + .col-lg-offset-10 { + margin-left: 83.3333333333%; + } + + .col-lg-offset-11 { + margin-left: 91.6666666667%; + } + + .col-lg-offset-12 { + margin-left: 100%; + } + } + + @media (min-width: 768px) { + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } + + .navbar-right .dropdown-menu-left { + left: 0; + right: auto; + } + } + @media (min-width: 768px) { + .nav-justified>li, + .nav-tabs.nav-justified>li { + display: table-cell; + width: 1%; + } + + .nav-justified>li>a, + .nav-tabs.nav-justified>li>a { + margin-bottom: 0; + } + } + @media (min-width: 768px) { + .nav-tabs-justified>li>a, + .nav-tabs.nav-justified>li>a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + + .nav-tabs-justified>.active>a, + .nav-tabs.nav-justified>.active>a, + .nav-tabs-justified>.active>a:hover, + .nav-tabs-justified>.active>a:focus { + border-bottom-color: #fff; + } + } + @media (min-width: 768px) { + .navbar { + border-radius: 0; + } + } + @media (min-width: 768px) { + .navbar-header { + float: left; + } + } + @media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + box-shadow: none; + } + + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } + + .navbar-collapse.in { + overflow-y: visible; + } + + .navbar-fixed-top .navbar-collapse, + .navbar-static-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + padding-right: 0; + padding-left: 0; + } + } + @media (max-device-width: 480px) and (orientation: landscape) { + .navbar-fixed-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + max-height: 200px; + } + } + @media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } + } + @media (min-width: 768px) { + .container>.navbar-header, + .container>.navbar-collapse, + .container-fluid>.navbar-header, + main>.navbar-header, + .container-fluid>.navbar-collapse, + main>.navbar-collapse { + margin-right: 0; + margin-left: 0; + } + } + @media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } + } + @media (min-width: 768px) { + .navbar>.container .navbar-brand, + .navbar>.container-fluid .navbar-brand, + .navbar>main .navbar-brand { + margin-left: -15px; + } + } + @media (min-width: 768px) { + .navbar-toggle { + display: none; + } + } + @media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; + } + + .navbar-nav .open .dropdown-menu>li>a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + + .navbar-nav .open .dropdown-menu>li>a { + line-height: 20px; + } + + .navbar-nav .open .dropdown-menu>li>a:hover, + .navbar-nav .open .dropdown-menu>li>a:focus { + background-image: none; + } + } + @media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + + .navbar-nav>li { + float: left; + } + + .navbar-nav>li>a { + padding-top: 15px; + padding-bottom: 15px; + } + } + @media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + + .navbar-form .form-control, + .navbar-form .form-action { + display: inline-block; + width: auto; + vertical-align: middle; + } + + .navbar-form .form-control-static { + display: inline-block; + } + + .navbar-form .input-group { + display: inline-table; + vertical-align: middle; + } + + .navbar-form .input-group .input-group-addon, + .navbar-form .input-group .input-group-btn, + .navbar-form .input-group .form-control, + .navbar-form .input-group .form-action { + width: auto; + } + + .navbar-form .input-group>.form-control, + .navbar-form .input-group>.form-action { + width: 100%; + } + + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } + + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + + .navbar-form .radio label, + .navbar-form .checkbox label { + padding-left: 0; + } + + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + + .navbar-form .has-feedback .form-control-feedback { + top: 0; + } + } + @media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } + + .navbar-form .form-group:last-child { + margin-bottom: 0; + } + } + @media (min-width: 768px) { + .navbar-form { + width: auto; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 0; + box-shadow: none; + } + } + @media (min-width: 768px) { + .navbar-text { + float: left; + margin-right: 15px; + margin-left: 15px; + } + } + @media (min-width: 768px) { + .navbar-left { + float: left !important; + } + + .navbar-right { + float: right !important; + margin-right: -15px; + } + + .navbar-right~.navbar-right { + margin-right: 0; + } + } + @media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu>li>a { + color: #999999; + } + + .navbar-default .navbar-nav .open .dropdown-menu>li>a:hover, + .navbar-default .navbar-nav .open .dropdown-menu>li>a:focus { + color: #61B44B; + background-color: transparent; + } + + .navbar-default .navbar-nav .open .dropdown-menu>.active>a, + .navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover, + .navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus { + color: #000; + background-color: transparent; + } + + .navbar-default .navbar-nav .open .dropdown-menu>.disabled>a, + .navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover, + .navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus { + color: #D8D8D8; + background-color: transparent; + } + } + @media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header { + border-color: #2f6297; + } + + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: #2f6297; + } + + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a { + color: #fff; + } + + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus { + color: #fff; + background-color: #61B44B; + } + + .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a, + .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus { + color: #fff; + background-color: #1E5A96; + } + + .navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a, + .navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus { + color: #D8D8D8; + background-color: transparent; + } + } + + @media (max-width: 767px) { + .visible-xs { + display: block !important; + } + + table.visible-xs { + display: table !important; + } + + tr.visible-xs { + display: table-row !important; + } + + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } + } + @media (max-width: 767px) { + .visible-xs-block { + display: block !important; + } + } + @media (max-width: 767px) { + .visible-xs-inline { + display: inline !important; + } + } + @media (max-width: 767px) { + .visible-xs-inline-block { + display: inline-block !important; + } + } + @media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + + table.visible-sm { + display: table !important; + } + + tr.visible-sm { + display: table-row !important; + } + + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } + } + @media (min-width: 768px) and (max-width: 991px) { + .visible-sm-block { + display: block !important; + } + } + @media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline { + display: inline !important; + } + } + @media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline-block { + display: inline-block !important; + } + } + @media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + + table.visible-md { + display: table !important; + } + + tr.visible-md { + display: table-row !important; + } + + th.visible-md, + td.visible-md { + display: table-cell !important; + } + } + @media (min-width: 992px) and (max-width: 1199px) { + .visible-md-block { + display: block !important; + } + } + @media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline { + display: inline !important; + } + } + @media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline-block { + display: inline-block !important; + } + } + @media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + + table.visible-lg { + display: table !important; + } + + tr.visible-lg { + display: table-row !important; + } + + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } + } + @media (min-width: 1200px) { + .visible-lg-block { + display: block !important; + } + } + @media (min-width: 1200px) { + .visible-lg-inline { + display: inline !important; + } + } + @media (min-width: 1200px) { + .visible-lg-inline-block { + display: inline-block !important; + } + } + @media (max-width: 767px) { + .hidden-xs { + display: none !important; + } + } + @media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } + } + @media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } + } + @media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } + } + @media print { + .visible-print { + display: block !important; + } + + table.visible-print { + display: table !important; + } + + tr.visible-print { + display: table-row !important; + } + + th.visible-print, + td.visible-print { + display: table-cell !important; + } + } + @media print { + .visible-print-block { + display: block !important; + } + } + @media print { + .visible-print-inline { + display: inline !important; + } + } + @media print { + .visible-print-inline-block { + display: inline-block !important; + } + } + @media print { + .hidden-print { + display: none !important; + } + } + + @media print { + main { + margin: 0; + } + + .root { + display: block; + } + + a[href]:after { + content: ""; + } + + abbr[title]:after { + content: ""; + } + } + + @media (min-width: 768px) { + #main-header { + position: sticky; + z-index: 11; + top: 0; + } + } + + %body.root.login{:style => "box-sizing: border-box; margin: 0; font-family: Roboto, Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.428571429; color: #4A4A4A; background-color: #fff; min-height: 100vh; display: flex; flex-direction: column;"} + %header#main-header{:style => "box-sizing: border-box; display: block;"} + #headerbar.navbar.navbar-default.lonely{:role => "navigation", :style => "box-sizing: border-box; position: relative; margin-bottom: 0; border: 1px solid transparent; background-color: #fff; border-color: #D8D8D8; z-index: 2; flex-shrink: 0; font-weight: 300; min-height: 40px; border-width: 0; border-bottom-width: 1px;"} + .container-fluid{:style => "box-sizing: border-box; padding-right: 15px; margin-right: auto; margin-left: auto; padding-left: 0;"} + .navbar-header{:style => "box-sizing: border-box; margin-right: -15px; margin-left: -15px;"} + .navbar-brand{:style => "box-sizing: border-box; float: left; padding: 15px 15px; line-height: 20px; color: #000; height: 40px; padding-top: 3px; padding-bottom: 10px; font-size: 22px; font-weight: 400;"} + = email_image_tag("logo.svg", width: 41, height: 40, style: "box-sizing: border-box; border: 0; vertical-align: middle; display: inline-block; position: relative; top: -3px; margin-left: 30px; margin-right: 20px;") + PuzzleTime + %main#content{:role => "main", :style => "box-sizing: border-box; display: block; padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; margin: 0 30px 40px; padding: 0; flex: 1;"} + = yield diff --git a/app/views/list/index.json.jbuilder b/app/views/list/index.json.jbuilder index cdb30e9cf..5bb40728a 100644 --- a/app/views/list/index.json.jbuilder +++ b/app/views/list/index.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.array!(entries) do |entry| json.extract! entry, :id, *default_crud_attrs json.url polymorphic_url(path_args(entry), format: :json) diff --git a/app/views/order_plannings/show.html.haml b/app/views/order_plannings/show.html.haml index 7a056cb71..e916d0695 100644 --- a/app/views/order_plannings/show.html.haml +++ b/app/views/order_plannings/show.html.haml @@ -6,4 +6,4 @@ = render 'orders/header' -= render file: 'plannings/base/show' += render template: 'plannings/base/show' diff --git a/app/views/order_plannings/show.js.haml b/app/views/order_plannings/show.js.haml index 19e27a41e..32c7cfa3b 100644 --- a/app/views/order_plannings/show.js.haml +++ b/app/views/order_plannings/show.js.haml @@ -4,4 +4,4 @@ -# https://github.com/puzzle/puzzletime. -= render file: 'plannings/base/show' += render template: 'plannings/base/show' diff --git a/app/views/orders/_submenu.html.haml b/app/views/orders/_submenu.html.haml index a33930c9c..17212f2f9 100644 --- a/app/views/orders/_submenu.html.haml +++ b/app/views/orders/_submenu.html.haml @@ -12,10 +12,10 @@ reports_orders_path(clear: 1), reports_orders_path -- if can?(:managed, Evaluation) +- if can?(:managed, Evaluations::Evaluation) = nav 'Meine Aufträge', evaluation_path('managed', clear: 1) -- if can?(:clients, Evaluation) +- if can?(:clients, Evaluations::Evaluation) = nav 'Kunden', evaluation_path('clients', clear: 1), evaluation_path('clients'), @@ -23,7 +23,7 @@ evaluation_path('workitememployees'), evaluation_path('subworkitems') -- if can?(:departments, Evaluation) +- if can?(:departments, Evaluations::Evaluation) = nav 'Organisationseinheiten', evaluation_path('departments', clear: 1), evaluation_path('departments'), diff --git a/app/views/orders/search.json.jbuilder b/app/views/orders/search.json.jbuilder index 022e7c7d0..491c6eca4 100644 --- a/app/views/orders/search.json.jbuilder +++ b/app/views/orders/search.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.array! @orders do |order| json.id order.id json.name order.name diff --git a/app/views/plannings/base/_submenu.html.haml b/app/views/plannings/base/_submenu.html.haml index a2a510db5..239bef2c3 100644 --- a/app/views/plannings/base/_submenu.html.haml +++ b/app/views/plannings/base/_submenu.html.haml @@ -8,7 +8,7 @@ - if current_user.order_responsible? = nav 'Meine Aufträge', plannings_orders_path(mine: true) - if can?(:read, Planning) - = nav 'Members', plannings_employees_path, plannings_employees_path + = nav 'Members', plannings_employees_path, plannings_employees_path, except: plannings_employee_path(current_user) = nav 'Aufträge', plannings_orders_path, plannings_orders_path = nav 'Organisationseinheiten', plannings_departments_path, plannings_departments_path = nav 'Unternehmen', plannings_company_path diff --git a/app/views/plannings/orders/update.js.haml b/app/views/plannings/orders/update.js.haml index 11262ccf1..32fe43c5e 100644 --- a/app/views/plannings/orders/update.js.haml +++ b/app/views/plannings/orders/update.js.haml @@ -4,7 +4,7 @@ -# https://github.com/puzzle/puzzletime. -= render file: 'plannings/base/update' += render template: 'plannings/base/update' - @board.included_accounting_posts.each do |post| $('#group_header_times_#{dom_id(post)}').html('#{j(render('accounting_post_total', post: post))}'); \ No newline at end of file diff --git a/app/views/shared/_error_messages.html.haml b/app/views/shared/_error_messages.html.haml index 7448fb7b7..c03ea5bbe 100644 --- a/app/views/shared/_error_messages.html.haml +++ b/app/views/shared/_error_messages.html.haml @@ -8,6 +8,8 @@ - if errors.any? #error_explanation.alert.alert-danger %ul - - errors.each do |attr, msg| + - errors.each do |error| + - attr = error.attribute + - msg = error.message %li = /^[[:upper:]]/.match(msg) ? msg : errors.full_message(attr, msg) diff --git a/app/views/shared/_submenu_employees.html.haml b/app/views/shared/_submenu_employees.html.haml index 9ec65481e..8d4d17ad7 100644 --- a/app/views/shared/_submenu_employees.html.haml +++ b/app/views/shared/_submenu_employees.html.haml @@ -8,7 +8,7 @@ employee_master_data_path, employee_master_data_path -- if can?(:employees, Evaluation) +- if can?(:employees, Evaluations::Evaluation) = nav 'Zeiten', evaluation_path('employees', clear: 1), evaluation_path('employees'), @@ -16,7 +16,7 @@ evaluation_path('employeesubworkitems'), '/weekly_graph' -- if can?(:absences, Evaluation) +- if can?(:absences, Evaluations::Evaluation) = nav 'Absenzen', evaluation_path('absences', clear: 1), evaluation_path('absences'), @@ -27,10 +27,10 @@ = nav 'Ferienplan', vacations_path --if can?(:export_report, Evaluation) +-if can?(:export_report, Evaluations::Evaluation) = nav 'Exporte', reports_export_path - if Settings.meal_compensation.active - - if can?(:meal_compensation, Evaluation) + - if can?(:meal_compensation, Evaluations::Evaluation) = nav 'Verpflegungsentschädigungen', meal_compensations_path diff --git a/app/views/vacations/show.html.haml b/app/views/vacations/show.html.haml index f9ef55e0e..409d29e71 100644 --- a/app/views/vacations/show.html.haml +++ b/app/views/vacations/show.html.haml @@ -53,10 +53,10 @@ %th{colspan: 2} %b= Absence.model_name.human(count: 2) %tr - %td= timebox_div Timebox.new(nil, '#cc2767', 10, '') + %td= timebox_div Graphs::Timebox.new(nil, '#cc2767', 10, '') %td Abwesenheit %tr - %td= timebox_div Timebox.new(nil, '#cc9557', 10, '') + %td= timebox_div Graphs::Timebox.new(nil, '#cc9557', 10, '') %td Unbezahlter Urlaub - if @user.management diff --git a/app/views/weekly_graph/_legend_item.html.haml b/app/views/weekly_graph/_legend_item.html.haml index a0d22e9f6..b12f1da28 100644 --- a/app/views/weekly_graph/_legend_item.html.haml +++ b/app/views/weekly_graph/_legend_item.html.haml @@ -5,5 +5,5 @@ %tr - %td= timebox_div Timebox.new(nil, legend_item[1], 10, '') + %td= timebox_div Graphs::Timebox.new(nil, legend_item[1], 10, '') %td= legend_item[0] diff --git a/app/views/weekly_graph/show.html.haml b/app/views/weekly_graph/show.html.haml index 41791029b..ef4e4a9da 100644 --- a/app/views/weekly_graph/show.html.haml +++ b/app/views/weekly_graph/show.html.haml @@ -29,7 +29,7 @@ %b Weiteres = render(partial: 'legend_item', collection: [['Wochenende / Feiertag', '#DDDDDD' ]]) %tr - %td= timebox_div Timebox.new(nil, '#FF0000', 1, '') + %td= timebox_div Graphs::Timebox.new(nil, '#FF0000', 1, '') %td Tagessoll (#{format_hour(WorkingCondition.value_at(@graph.period.start_date, :must_hours_per_day) * @graph.must_hours_factor)} ) diff --git a/app/views/work_items/search.json.jbuilder b/app/views/work_items/search.json.jbuilder index 3d8ea0c96..e063dab95 100644 --- a/app/views/work_items/search.json.jbuilder +++ b/app/views/work_items/search.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.array! @work_items do |item| json.id item.id json.name item.name diff --git a/app/views/workload_report/_filters.html.haml b/app/views/workload_report/_filters.html.haml index addfa8dbb..bf405a686 100644 --- a/app/views/workload_report/_filters.html.haml +++ b/app/views/workload_report/_filters.html.haml @@ -5,9 +5,6 @@ = form_tag(nil, method: :get, class: 'form-inline', role: 'filter', remote: true, data: { spin: true }) do - = direct_filter_date(:start_date, 'Von', @period.start_date) - = direct_filter_date(:end_date, 'Bis', @period.end_date) - = direct_filter_select(:department_id, 'OE', @departments) .form-group diff --git a/app/views/workload_report/index.html.haml b/app/views/workload_report/index.html.haml index 2491b1ea6..99e3e3fb2 100644 --- a/app/views/workload_report/index.html.haml +++ b/app/views/workload_report/index.html.haml @@ -7,10 +7,11 @@ - @title ||= 'Auslastung' %p= render 'filters' += render 'evaluator/period_link', entities_name: 'Absenzen' .results= render 'results' -- if can?(:capacity_report, Evaluation) +- if can?(:capacity_report, Evaluations::Evaluation) = action_link(action_icon('export', 'CSV Detaillierte Auslastung'), reports_capacity_path, data: { turbolinks: false }) diff --git a/bin/bundle b/bin/bundle index f19acf5b5..374a0a1fa 100755 --- a/bin/bundle +++ b/bin/bundle @@ -1,3 +1,114 @@ #!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -load Gem.bin_path('bundler', 'bundle') +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../../Gemfile", __FILE__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") + + requirement += ".a" if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/rails b/bin/rails index 073966023..efc037749 100755 --- a/bin/rails +++ b/bin/rails @@ -1,4 +1,4 @@ #!/usr/bin/env ruby -APP_PATH = File.expand_path('../config/application', __dir__) -require_relative '../config/boot' -require 'rails/commands' +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake index 17240489f..4fbf10b96 100755 --- a/bin/rake +++ b/bin/rake @@ -1,4 +1,4 @@ #!/usr/bin/env ruby -require_relative '../config/boot' -require 'rake' +require_relative "../config/boot" +require "rake" Rake.application.run diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 000000000..369a05bed --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rubocop' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup index b2293a35a..3cd5a9d78 100755 --- a/bin/setup +++ b/bin/setup @@ -1,37 +1,33 @@ #!/usr/bin/env ruby -require 'pathname' -require 'fileutils' -include FileUtils +require "fileutils" # path to your application root. -APP_ROOT = Pathname.new File.expand_path('..', __dir__) +APP_ROOT = File.expand_path("..", __dir__) def system!(*args) - system(*args) || abort("\n== Command #{args} failed ==") + system(*args, exception: true) end -chdir APP_ROOT do - # This script is a starting point to setup your application. +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. - puts '== Installing dependencies ==' - system! 'gem install bundler --conservative' - system('bundle check') || system!('bundle install') - - # Install JavaScript dependencies if using Yarn - # system('bin/yarn') + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") # puts "\n== Copying sample files ==" - # unless File.exist?('config/database.yml') - # cp 'config/database.yml.sample', 'config/database.yml' + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" # end puts "\n== Preparing database ==" - system! 'bin/rails db:setup' + system! "bin/rails db:prepare" puts "\n== Removing old logs and tempfiles ==" - system! 'bin/rails log:clear tmp:clear' + system! "bin/rails log:clear tmp:clear" puts "\n== Restarting application server ==" - system! 'bin/rails restart' + system! "bin/rails restart" end diff --git a/bin/spring b/bin/spring deleted file mode 100755 index d89ee495f..000000000 --- a/bin/spring +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env ruby - -# This file loads Spring without using Bundler, in order to be fast. -# It gets overwritten when you run the `spring binstub` command. - -unless defined?(Spring) - require 'rubygems' - require 'bundler' - - lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) - spring = lockfile.specs.detect { |spec| spec.name == 'spring' } - if spring - Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path - gem 'spring', spring.version - require 'spring/binstub' - end -end diff --git a/bin/yarn b/bin/yarn index 460dd565b..9fab2c350 100755 --- a/bin/yarn +++ b/bin/yarn @@ -1,9 +1,15 @@ #!/usr/bin/env ruby APP_ROOT = File.expand_path('..', __dir__) Dir.chdir(APP_ROOT) do - begin - exec "yarnpkg", *ARGV - rescue Errno::ENOENT + yarn = ENV["PATH"].split(File::PATH_SEPARATOR). + select { |dir| File.expand_path(dir) != __dir__ }. + product(["yarn", "yarn.cmd", "yarn.ps1"]). + map { |dir, file| File.expand_path(file, dir) }. + find { |file| File.executable?(file) } + + if yarn + exec yarn, *ARGV + else $stderr.puts "Yarn executable was not detected in the system." $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" exit 1 diff --git a/config.ru b/config.ru index bd83b2541..6dc832180 100644 --- a/config.ru +++ b/config.ru @@ -1,4 +1,8 @@ +# frozen_string_literal: true + # This file is used by Rack-based servers to start the application. -require ::File.expand_path('../config/environment', __FILE__) +require_relative 'config/environment' + run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb index f88aaeced..019b87121 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,9 +1,11 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of +# frozen_string_literal: true + +# Copyright (c) 2006-2023, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -require File.expand_path('../boot', __FILE__) +require_relative 'boot' require 'rails/all' @@ -17,26 +19,23 @@ module Puzzletime class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 5.2 + config.load_defaults 7.0 + # FIXME: remove this if it works flawlesly config.active_record.belongs_to_required_by_default = false - # Settings in config/environments/* take precedence over those specified here. - # Application configuration can go into files in config/initializers - # -- all .rb files in that directory are automatically loaded after loading - # the framework and any gems in your application. + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") - config.autoload_paths += %W(#{config.root}/app/domain/forms - #{config.root}/app/domain/reports - #{config.root}/app/models/util - #{config.root}/app/domain/evaluations - #{config.root}/app/domain/graphs - #{config.root}/app/domain/presenters - #{config.root}/app/domain - #{config.root}/app/jobs) + config.autoload_paths += %W[#{config.root}/app/models/util] # Use custom error controller - config.exceptions_app = self.routes + config.exceptions_app = routes # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. @@ -60,19 +59,29 @@ class Application < Rails::Application memcached_host = ENV['RAILS_MEMCACHED_HOST'] || 'localhost' memcached_port = ENV['RAILS_MEMCACHED_PORT'] || '11211' - config.cache_store = :dalli_store, "#{memcached_host}:#{memcached_port}" + config.cache_store = :mem_cache_store, "#{memcached_host}:#{memcached_port}" config.middleware.insert_before Rack::ETag, Rack::Deflater - config.active_record.time_zone_aware_types = [:datetime, :time] + config.active_record.time_zone_aware_types = %i[datetime time] config.active_job.queue_adapter = :delayed_job + config.action_mailer.default_url_options = { + protocol: 'https', + host: ENV['RAILS_MAIL_URL_HOST'].presence || 'example.com' + } + config.to_prepare do |_| Crm.init Invoicing.init - BI.init + CommitReminderJob.schedule + rescue ActiveRecord::StatementInvalid => e + # the db might not exist yet, lets ignore the error in this case + raise e unless e.message.include?('PG::UndefinedTable') || e.message.include?('does not exist') end + + config.active_record.yaml_column_permitted_classes = [Date, BigDecimal] end def self.version @@ -84,18 +93,16 @@ def self.changelog_url @@ptime_changelog_url ||= "https://github.com/puzzle/puzzletime/blob/#{commit_hash || 'master'}/CHANGELOG.md" end - private - def self.build_version Puzzletime::VERSION end def self.commit_hash(short: false) - if File.exists?("#{Rails.root}/BUILD_INFO") - commit = File.open("#{Rails.root}/BUILD_INFO").first.chomp - return commit.first(7) if short + return unless File.exist?(Rails.root.join('BUILD_INFO').to_s) - commit - end + commit = File.open(Rails.root.join('BUILD_INFO').to_s).first.chomp + return commit.first(7) if short + + commit end end diff --git a/config/boot.rb b/config/boot.rb index b9e460cef..c04863fa7 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/config/cable.yml b/config/cable.yml index 4400e072b..60726b3cc 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -2,7 +2,7 @@ development: adapter: async test: - adapter: async + adapter: test production: adapter: redis diff --git a/config/environment.rb b/config/environment.rb index e76ccaf86..d5abe5580 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,8 +1,7 @@ +# frozen_string_literal: true + # Load the Rails application. require_relative 'application' -# FIXME: ignoring deprecation warnings for now. -::ActiveSupport::Deprecation.silenced = true if Rails.env.test? - # Initialize the Rails application. Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb index d60823a3a..a8140f8cd 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,26 +1,41 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of +# frozen_string_literal: true + +# Copyright (c) 2006-2023, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. +require 'active_support/core_ext/integer/time' + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # In the development environment your application's code is reloaded on - # every request. This slows down response time but is perfect for development + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false - # Show full error reports and disable caching. + # Show full error reports. config.consider_all_requests_local = true + # Enable server timing + config.server_timing = true + + config.hosts = [ + '127.0.0.1', + 'localhost', + '.local', + ENV.fetch('RAILS_HOSTS', nil) + ] + # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. # if Rails.root.join('tmp', 'caching-dev.txt').exist? # config.action_controller.perform_caching = true + # config.action_controller.enable_fragment_cache_logging = true # # config.cache_store = :memory_store # config.public_file_server.headers = { @@ -32,36 +47,31 @@ # config.cache_store = :null_store # end - # Store uploaded files on the local file system (see config/storage.yml for options) - config.active_storage.service = ENV.fetch('RAILS_STORAGE_SERVICE', 'local').to_sym + # config.session_store(:cookie_store, key: '_app_session_dev') + config.session_store( + ActionDispatch::Session::CacheStore, + expire_after: 12.hours, + same_site: :lax, + secure: false + ) - # Perform caching as the session is stored there - config.action_controller.perform_caching = true + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = ENV.fetch('RAILS_STORAGE_SERVICE', 'local').to_sym # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false config.action_mailer.perform_caching = false - # Mail sender - config.action_mailer.delivery_method = (ENV['RAILS_MAIL_DELIVERY_METHOD'].presence || :smtp).to_sym - - if ENV['RAILS_MAIL_DELIVERY_CONFIG'].present? - case config.action_mailer.delivery_method.to_s - when 'smtp' - config.action_mailer.smtp_settings = - YAML.load("{ #{ENV['RAILS_MAIL_DELIVERY_CONFIG']} }").symbolize_keys - when 'sendmail' - config.action_mailer.sendmail_settings = - YAML.load("{ #{ENV['RAILS_MAIL_DELIVERY_CONFIG']} }").symbolize_keys - end - else - config.action_mailer.smtp_settings = { :address => '127.0.0.1', :port => 1025 } - end - # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load @@ -73,17 +83,42 @@ # number of complex assets. config.assets.debug = true - config.middleware.insert_before ActionDispatch::Cookies, Rack::RequestProfiler, printer: RubyProf::CallStackPrinter - # Suppress logger output for asset requests. config.assets.quiet = true - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. - # config.file_watcher = ActiveSupport::EventedFileUpdateChecker + config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + config.middleware.insert_before ActionDispatch::Cookies, Rack::RequestProfiler, printer: RubyProf::CallStackPrinter + + # Perform caching as the session is stored there + config.action_controller.perform_caching = true + + # Mail sender + config.action_mailer.delivery_method = (ENV['RAILS_MAIL_DELIVERY_METHOD'].presence || :smtp).to_sym + + if ENV['RAILS_MAIL_DELIVERY_CONFIG'].present? + case config.action_mailer.delivery_method.to_s + when 'smtp' + config.action_mailer.smtp_settings = + YAML.load("{ #{ENV['RAILS_MAIL_DELIVERY_CONFIG']} }").symbolize_keys + when 'sendmail' + config.action_mailer.sendmail_settings = + YAML.load("{ #{ENV['RAILS_MAIL_DELIVERY_CONFIG']} }").symbolize_keys + end + else + config.action_mailer.smtp_settings = { address: '127.0.0.1', port: 1025 } + end # Raises error for missing translations # config.action_view.raise_on_missing_translations = true @@ -97,25 +132,25 @@ Bullet.add_footer = false Bullet.stacktrace_includes = [] - Bullet.add_whitelist type: :unused_eager_loading, class_name: "Ordertime", association: :employee - Bullet.add_whitelist type: :unused_eager_loading, class_name: "Ordertime", association: :absence - Bullet.add_whitelist type: :unused_eager_loading, class_name: "Absencetime", association: :work_item - Bullet.add_whitelist type: :unused_eager_loading, class_name: "Planning", association: :work_item - Bullet.add_whitelist type: :unused_eager_loading, class_name: "Planning", association: :employee - Bullet.add_whitelist type: :unused_eager_loading, class_name: "Order", association: :contacts - Bullet.add_whitelist type: :unused_eager_loading, class_name: "Order", association: :work_item - Bullet.add_whitelist type: :unused_eager_loading, class_name: "Order", association: :kind - Bullet.add_whitelist type: :unused_eager_loading, class_name: "Order", association: :department - Bullet.add_whitelist type: :unused_eager_loading, class_name: "Order", association: :status - Bullet.add_whitelist type: :unused_eager_loading, class_name: "Order", association: :responsible - Bullet.add_whitelist type: :unused_eager_loading, class_name: "Order", association: :targets - Bullet.add_whitelist type: :unused_eager_loading, class_name: "Order", association: :order_uncertainties - - Bullet.add_whitelist type: :n_plus_one_query, class_name: "Order", association: :order_team_members - Bullet.add_whitelist type: :n_plus_one_query, class_name: "Order", association: :team_members - Bullet.add_whitelist type: :n_plus_one_query, class_name: "Order", association: :order_contacts - Bullet.add_whitelist type: :n_plus_one_query, class_name: "WorkItem", association: :parent - Bullet.add_whitelist type: :n_plus_one_query, class_name: "BillingAddress", association: :client - Bullet.add_whitelist type: :n_plus_one_query, class_name: "BillingAddress", association: :contact + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Ordertime', association: :employee + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Ordertime', association: :absence + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Absencetime', association: :work_item + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Planning', association: :work_item + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Planning', association: :employee + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Order', association: :contacts + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Order', association: :work_item + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Order', association: :kind + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Order', association: :department + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Order', association: :status + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Order', association: :responsible + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Order', association: :targets + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Order', association: :order_uncertainties + Bullet.add_safelist type: :unused_eager_loading, class_name: 'Expense', association: :reviewer + Bullet.add_safelist type: :n_plus_one_query, class_name: 'Order', association: :order_team_members + Bullet.add_safelist type: :n_plus_one_query, class_name: 'Order', association: :team_members + Bullet.add_safelist type: :n_plus_one_query, class_name: 'Order', association: :order_contacts + Bullet.add_safelist type: :n_plus_one_query, class_name: 'WorkItem', association: :parent + Bullet.add_safelist type: :n_plus_one_query, class_name: 'BillingAddress', association: :client + Bullet.add_safelist type: :n_plus_one_query, class_name: 'BillingAddress', association: :contact end end diff --git a/config/environments/production.rb b/config/environments/production.rb index 7b1ef82c9..c0e0d7b31 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,8 +1,12 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of +# frozen_string_literal: true + +# Copyright (c) 2006-2023, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. +require 'active_support/core_ext/integer/time' + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -22,98 +26,87 @@ # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true - config.read_encrypted_secrets = false # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? - # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass + config.assets.js_compressor = :terser # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = 'http://assets.example.com' + # config.asset_host = 'http://assets.example.com' # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX config.action_dispatch.x_sendfile_header = ENV['RAILS_X_SENDFILE_HEADER'] || 'X-Sendfile' - # Store uploaded files on the local file system (see config/storage.yml for options) + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = ENV.fetch('RAILS_STORAGE_SERVICE', 'ocp4_s3').to_sym - # Mount Action Cable outside main process or domain + # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - ssl = %w[true yes 1].include?(ENV['RAILS_HOST_SSL']) + # config.force_ssl = true + ssl = %w[true yes 1].include?(ENV.fetch('RAILS_HOST_SSL', nil)) config.force_ssl = ssl config.ssl_options = { redirect: { exclude: ->(request) { request.path =~ /health|readiness/ } } } - # Set to :debug to see everything in the log. + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). config.log_level = :info # Prepend all log lines with the following tags. config.log_tags = [:request_id] - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) - # Use a different cache store in production. # config.cache_store = :mem_cache_store - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = "http://assets.example.com" - - # Precompile additional assets. - # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. - # config.assets.precompile += %w( search.js ) + config.session_store( + ActionDispatch::Session::CacheStore, + expire_after: 12.hours, + same_site: :lax, + secure: true + ) - # Use a real queuing backend for Active Job (and separate queues per environment) + # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "puzzletime_#{Rails.env}" + # config.active_job.queue_name_prefix = "puzzletime_production" + config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. - config.action_mailer.raise_delivery_errors = false - - # Mail sender - config.action_mailer.delivery_method = (ENV['RAILS_MAIL_DELIVERY_METHOD'].presence || :smtp).to_sym - - if ENV['RAILS_MAIL_DELIVERY_CONFIG'].present? - case config.action_mailer.delivery_method - when :smtp - config.action_mailer.smtp_settings = - YAML.load("{ #{ENV['RAILS_MAIL_DELIVERY_CONFIG']} }").symbolize_keys - when :sendmail - config.action_mailer.sendmail_settings = - YAML.load("{ #{ENV['RAILS_MAIL_DELIVERY_CONFIG']} }").symbolize_keys - end - end + # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). - config.i18n.fallbacks = [I18n.default_locale] + config.i18n.fallbacks = true # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify + # Log disallowed deprecations. + config.active_support.disallowed_deprecation = :log + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new + config.log_formatter = Logger::Formatter.new # Use a different logger for distributed setups. - # require 'syslog/logger' + # require "syslog/logger" # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') if ENV['RAILS_LOG_TO_STDOUT'].present? @@ -124,4 +117,41 @@ # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false + + # Inserts middleware to perform automatic connection switching. + # The `database_selector` hash is used to pass options to the DatabaseSelector + # middleware. The `delay` is used to determine how long to wait after a write + # to send a subsequent read to the primary. + # + # The `database_resolver` class is used by the middleware to determine which + # database is appropriate to use based on the time delay. + # + # The `database_resolver_context` class is used by the middleware to set + # timestamps for the last write to the primary. The resolver uses the context + # class timestamps to determine how long to wait before reading from the + # replica. + # + # By default Rails will store a last write timestamp in the session. The + # DatabaseSelector middleware is designed as such you can define your own + # strategy for connection switching and pass that into the middleware through + # these configuration options. + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + + config.read_encrypted_secrets = false + + # Mail sender + config.action_mailer.delivery_method = (ENV['RAILS_MAIL_DELIVERY_METHOD'].presence || :smtp).to_sym + + if ENV['RAILS_MAIL_DELIVERY_CONFIG'].present? + case config.action_mailer.delivery_method + when :smtp + config.action_mailer.smtp_settings = + YAML.load("{ #{ENV['RAILS_MAIL_DELIVERY_CONFIG']} }").symbolize_keys + when :sendmail + config.action_mailer.sendmail_settings = + YAML.load("{ #{ENV['RAILS_MAIL_DELIVERY_CONFIG']} }").symbolize_keys + end + end end diff --git a/config/environments/test.rb b/config/environments/test.rb index f54f986dd..aa70f7369 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,17 +1,34 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of +# frozen_string_literal: true + +# Copyright (c) 2006-2023, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. +require 'active_support/core_ext/integer/time' + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! config.cache_classes = true + # config.session_store( + # ActionDispatch::Session::CacheStore, + # expire_after: 12.hours, + # same_site: :lax, + # secure: false + # ) + + config.cache_store = :memory_store + config.session_store(:mem_cache_store) + + # config.action_dispatch.cookies_serializer = :marshal + # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. @@ -20,7 +37,7 @@ # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. @@ -33,7 +50,7 @@ # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false - # Store uploaded files on the local file system in a temporary directory + # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = ENV.fetch('RAILS_STORAGE_SERVICE', 'test').to_sym config.action_mailer.perform_caching = false @@ -46,8 +63,15 @@ # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise - config.cache_store = :memory_store + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true end diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb index 89d2efab2..f4556db39 100644 --- a/config/initializers/application_controller_renderer.rb +++ b/config/initializers/application_controller_renderer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 94db4b818..85955c3b4 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. @@ -6,12 +8,13 @@ # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path # Add Yarn node_modules folder to the asset load path. -Rails.application.config.assets.paths << - Rails.root.join('app', 'assets', 'fonts') << +Rails.application.config.assets.paths += [ + Rails.root.join('app/assets/fonts'), Rails.root.join('node_modules') +] # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. -# Rails.application.config.assets.precompile += %w( search.js ) -Rails.application.config.assets.precompile += %w(print.css phone.css *.png *.gif *.jpg *.svg) +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) +Rails.application.config.assets.precompile += %w[print.css phone.css *.png *.gif *.jpg *.svg] diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb index 59385cdf3..d43cc1d2f 100644 --- a/config/initializers/backtrace_silencers.rb +++ b/config/initializers/backtrace_silencers.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. -# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } +# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } -# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. -# Rails.backtrace_cleaner.remove_silencers! +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code +# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". +Rails.backtrace_cleaner.remove_silencers! if ENV['BACKTRACE'] diff --git a/config/initializers/better_errors.rb b/config/initializers/better_errors.rb deleted file mode 100644 index c24744c35..000000000 --- a/config/initializers/better_errors.rb +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -if defined?(BetterErrors) && ENV['BETTER_ERRORS_URL'].present? - BetterErrors.editor = proc { |full_path, line| - namespace = OpenStruct.new(full_path: "/hello/world", line: 123) - Haml::Engine.new(ENV['BETTER_ERRORS_URL']).render(namespace.instance_eval { binding }) - } -end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index d3bcaa5ec..f3bcce546 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Define an application-wide content security policy @@ -11,6 +12,8 @@ # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https +# # If you are using webpack-dev-server then specify webpack-dev-server host +# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" @@ -19,6 +22,9 @@ # If you are using UJS then enable automatic nonce generation # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } +# Set the nonce only to specific directives +# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) + # Report CSP violations to a specified URI # For further information see the following documentation: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb index 5a6a32d37..ee8dff9c9 100644 --- a/config/initializers/cookies_serializer.rb +++ b/config/initializers/cookies_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. diff --git a/config/initializers/country_select.rb b/config/initializers/country_select.rb index 10c033cc7..27bd3af9a 100644 --- a/config/initializers/country_select.rb +++ b/config/initializers/country_select.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # Return a string to customize the text in the ').size + end + + test 'belongs_to_field with :list option' do + list = CrudTestModel.all + f = form.belongs_to_field(:companion_id, + list: [list.first, list.second]) + + assert_equal 3, f.scan('').size + end + + test 'belongs_to_field with instance variable' do + list = CrudTestModel.all + @companions = [list.first, list.second] + f = form.belongs_to_field(:companion_id) + + assert_equal 3, f.scan('').size + end + + test 'belongs_to_field with empty list' do + @companions = [] + f = form.belongs_to_field(:companion_id) + + assert_match t('global.associations.none_available'), f + assert_equal 0, f.scan('').size + end + + test 'has_and_belongs_to_many_field has all options by default' do + f = form.has_many_field(:other_ids) + + assert_equal 6, f.scan('').size + end + + test 'has_and_belongs_to_many_field with :list option' do + list = OtherCrudTestModel.all + f = form.has_many_field(:other_ids, list: [list.first, list.second]) + + assert_equal 2, f.scan('').size + end + + test 'has_and_belongs_to_many_field with instance variable' do + list = OtherCrudTestModel.all + @others = [list.first, list.second] + f = form.has_many_field(:other_ids) + + assert_equal 2, f.scan('').size + end + + test 'has_and_belongs_to_many_field with empty list' do + @others = [] + f = form.has_many_field(:other_ids) + + assert_match t('global.associations.none_available'), f + assert_equal 0, f.scan('').size + end + + test 'has_many_field has all options by default' do + f = form.has_many_field(:more_ids) + + assert_equal 6, f.scan('').size + end + + test 'has_many_field with :list option' do + list = OtherCrudTestModel.all + f = form.has_many_field(:more_ids, list: [list.first, list.second]) + + assert_equal 2, f.scan('').size + end + + test 'has_many_field with instance variable' do + list = OtherCrudTestModel.all + @mores = [list.first, list.second] + f = form.has_many_field(:more_ids) + + assert_equal 2, f.scan('').size + end - test 'input_field dispatches belongs_to attr to select field' do - assert_equal form.belongs_to_field(:companion_id), - form.input_field(:companion_id) - assert form.belongs_to_field(:companion_id).html_safe? - end + test 'has_many_field with empty list' do + @mores = [] + f = form.has_many_field(:more_ids) - test 'input_field dispatches has_and_belongs_to_many attr to select field' do - assert_equal form.has_many_field(:other_ids), - form.input_field(:other_ids) - assert form.has_many_field(:other_ids).html_safe? - end + assert_match t('global.associations.none_available'), f + assert_equal 0, f.scan('').size + end + + test 'string_field sets maxlength attribute if limit' do + assert_match(/maxlength="50"/, form.string_field(:name)) + end + + test 'label creates captionized label' do + assert_match(/label [^>]*for.+Gugus dada/, form.label(:gugus_dada)) + assert_predicate form.label(:gugus_dada), :html_safe? + end + + test 'classic label still works' do + assert_match(/label [^>]*for.+hoho/, form.label(:gugus_dada, 'hoho')) + assert_predicate form.label(:gugus_dada, 'hoho'), :html_safe? + end + + test 'labeled_text_field create label' do + assert_match(/label [^>]*for.+input/m, form.labeled_string_field(:name)) + assert_predicate form.labeled_string_field(:name), :html_safe? + end + + test 'labeled field creates label' do + result = form.labeled('gugus', + "".html_safe) - test 'input_field dispatches has_many attr to select field' do - assert_equal form.has_many_field(:more_ids), - form.input_field(:more_ids) - assert form.has_many_field(:more_ids).html_safe? - end - - test 'input_fields concats multiple fields' do - result = form.labeled_input_fields(:name, :remarks, :children) - assert result.html_safe? - assert result.include?(form.input_field(:name, required: 'required')) - assert result.include?(form.input_field(:remarks)) - assert result.include?(form.input_field(:children)) - end - - test 'labeld_input_field adds required mark' do - result = form.labeled_input_field(:name) - assert result.include?('input-group-addon') - result = form.labeled_input_field(:remarks) - assert !result.include?('input-group-addon') - end if false - - test 'labeld_input_field adds help text' do - result = form.labeled_input_field(:name, help: 'Some Help') - assert result.include?(form.help_block('Some Help')) - assert result.include?('input-group-addon') - end if false - - test 'belongs_to_field has all options by default' do - f = form.belongs_to_field(:companion_id) - assert_equal 7, f.scan('').size - end - - test 'belongs_to_field with :list option' do - list = CrudTestModel.all - f = form.belongs_to_field(:companion_id, - list: [list.first, list.second]) - assert_equal 3, f.scan('').size - end - - test 'belongs_to_field with instance variable' do - list = CrudTestModel.all - @companions = [list.first, list.second] - f = form.belongs_to_field(:companion_id) - assert_equal 3, f.scan('').size - end - - test 'belongs_to_field with empty list' do - @companions = [] - f = form.belongs_to_field(:companion_id) - assert_match t('global.associations.none_available'), f - assert_equal 0, f.scan('').size - end - - test 'has_and_belongs_to_many_field has all options by default' do - f = form.has_many_field(:other_ids) - assert_equal 6, f.scan('').size - end - - test 'has_and_belongs_to_many_field with :list option' do - list = OtherCrudTestModel.all - f = form.has_many_field(:other_ids, list: [list.first, list.second]) - assert_equal 2, f.scan('').size - end - - test 'has_and_belongs_to_many_field with instance variable' do - list = OtherCrudTestModel.all - @others = [list.first, list.second] - f = form.has_many_field(:other_ids) - assert_equal 2, f.scan('').size - end - - test 'has_and_belongs_to_many_field with empty list' do - @others = [] - f = form.has_many_field(:other_ids) - assert_match t('global.associations.none_available'), f - assert_equal 0, f.scan('').size - end - - test 'has_many_field has all options by default' do - f = form.has_many_field(:more_ids) - assert_equal 6, f.scan('').size - end - - test 'has_many_field with :list option' do - list = OtherCrudTestModel.all - f = form.has_many_field(:more_ids, list: [list.first, list.second]) - assert_equal 2, f.scan('').size - end - - test 'has_many_field with instance variable' do - list = OtherCrudTestModel.all - @mores = [list.first, list.second] - f = form.has_many_field(:more_ids) - assert_equal 2, f.scan('').size - end - - test 'has_many_field with empty list' do - @mores = [] - f = form.has_many_field(:more_ids) - assert_match t('global.associations.none_available'), f - assert_equal 0, f.scan('').size - end - - test 'string_field sets maxlength attribute if limit' do - assert_match /maxlength="50"/, form.string_field(:name) - end - - test 'label creates captionized label' do - assert_match /label [^>]*for.+Gugus dada/, form.label(:gugus_dada) - assert form.label(:gugus_dada).html_safe? - end - - test 'classic label still works' do - assert_match /label [^>]*for.+hoho/, form.label(:gugus_dada, 'hoho') - assert form.label(:gugus_dada, 'hoho').html_safe? - end - - test 'labeled_text_field create label' do - assert_match /label [^>]*for.+input/m, form.labeled_string_field(:name) - assert form.labeled_string_field(:name).html_safe? - end - - test 'labeled field creates label' do - result = form.labeled('gugus', - "".html_safe) - assert result.html_safe? - assert_match /label [^>]*for.+".html_safe - end - assert result.html_safe? - assert_match /label [^>]*for.+".html_safe, - caption: 'Caption') - assert result.html_safe? - assert_match /label [^>]*for.+>Caption<\/label>.*".html_safe - end - assert result.html_safe? - assert_match /label [^>]*for.+>Caption<\/label>.*]*for.+".html_safe + end + + assert_predicate result, :html_safe? + assert_match(/label [^>]*for.+".html_safe, + caption: 'Caption') + + assert_predicate result, :html_safe? + assert_match(%r{label [^>]*for.+>Caption.*".html_safe + end + + assert_predicate result, :html_safe? + assert_match(%r{label [^>]*for.+>Caption.*Upcase', col.html_header - assert_equal 'FOO', col.content('foo') - assert_equal 'FOO', col.html_cell('foo') - end + assert_equal 'Upcase', col.html_header + assert_equal 'FOO', col.content('foo') + assert_equal 'FOO', col.html_cell('foo') + end - test 'attr col content with custom format_size method' do - table.attrs :size - col = table.cols.first + test 'attr col content with custom format_size method' do + table.attrs :size + col = table.cols.first - assert_equal '4 chars', col.content('abcd') - end + assert_equal '4 chars', col.content('abcd') + end - test 'two x two table' do - dom = <<-FIN + test 'two x two table' do + dom = <<-FIN @@ -74,16 +78,16 @@ def format_size(obj)
    UpcaseSize
    BAHR4 chars
    - FIN - dom.gsub!(/[\n\t]/, '').gsub!(/\s{2,}/, '') + FIN + dom = dom.gsub(/[\n\t]/, '').gsub(/\s{2,}/, '') - table.attrs :upcase, :size + table.attrs :upcase, :size - assert_dom_equal dom, table.to_html - end + assert_dom_equal dom, table.to_html + end - test 'table with before and after cells' do - dom = <<-FIN + test 'table with before and after cells' do + dom = <<-FIN @@ -103,18 +107,18 @@ def format_size(obj)
    headUpcaseSize
    - FIN - dom.gsub!(/[\n\t]/, '').gsub!(/\s{2,}/, '') + FIN + dom = dom.gsub(/[\n\t]/, '').gsub(/\s{2,}/, '') - table.col('head', class: 'left') { |e| link_to e, '/' } - table.attrs :upcase, :size - table.col { |e| "Never #{e}" } + table.col('head', class: 'left') { |e| link_to e, '/' } + table.attrs :upcase, :size + table.col { |e| "Never #{e}" } - assert_dom_equal dom, table.to_html - end + assert_dom_equal dom, table.to_html + end - test 'empty entries collection renders empty table' do - dom = <<-FIN + test 'empty entries collection renders empty table' do + dom = <<-FIN @@ -122,14 +126,16 @@ def format_size(obj)
    headUpcaseSize
    - FIN - dom.gsub!(/[\n\t]/, '').gsub!(/\s{2,}/, '') + FIN + dom = dom.gsub(/[\n\t]/, '').gsub(/\s{2,}/, '') - table = DryCrud::Table::Builder.new([], self) - table.col('head', class: 'left') { |e| link_to e, '/' } - table.attrs :upcase, :size - table.col { |e| "Never #{e}" } + table = DryCrud::Table::Builder.new([], self) + table.col('head', class: 'left') { |e| link_to e, '/' } + table.attrs :upcase, :size + table.col { |e| "Never #{e}" } - assert_dom_equal dom, table.to_html + assert_dom_equal dom, table.to_html + end + end end end diff --git a/test/helpers/employee_master_data_helper_test.rb b/test/helpers/employee_master_data_helper_test.rb index fba34718f..201805d9e 100644 --- a/test/helpers/employee_master_data_helper_test.rb +++ b/test/helpers/employee_master_data_helper_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2020, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -9,7 +11,8 @@ class EmployeeMasterDataHelperTest < ActionView::TestCase include EmployeeMasterDataHelper test '#format_year_of_service' do - employment_date = Date.tomorrow - 4.years - assert_equal format_year_of_service(employment_date), 3 + employment_date = 4.years.ago.tomorrow + + assert_equal 3, format_year_of_service(employment_date) end end diff --git a/test/helpers/form_helper_test.rb b/test/helpers/form_helper_test.rb index 617f40464..0ecaaad2a 100644 --- a/test/helpers/form_helper_test.rb +++ b/test/helpers/form_helper_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -25,15 +27,15 @@ class FormHelperTest < ActionView::TestCase end end - assert_match /form .*?action="\/crud_test_models\/#{e.id}" - .*?method="post"/x, f - assert_match /form .*?class="special\ form-horizontal"/x, f - assert_match /input .*?type="hidden" + assert_match(%r{form .*?action="/crud_test_models/#{e.id}" + .*?method="post"}x, f) + assert_match(/form .*?class="special\ form-horizontal"/x, f) + assert_match(/input .*?type="hidden" .*?name="_method" - .*?value="(patch|put)"/x, f - assert_match /input .*?type="text" + .*?value="(patch|put)"/x, f) + assert_match(/input .*?type="text" .*?value="AAAAA" - .*?name="crud_test_model\[name\]"/x, f + .*?name="crud_test_model\[name\]"/x, f) end test 'standard form' do @@ -47,31 +49,32 @@ class FormHelperTest < ActionView::TestCase end end - assert_match /form .*?action="\/crud_test_models\/#{e.id}" - .*?method="post"/x, f - assert_match /form .*?class="special\ form-horizontal"/x, f - assert_match /input .*?type="hidden" + assert_match(%r{form .*?action="/crud_test_models/#{e.id}" + .*?method="post"}x, f) + assert_match(/form .*?class="special\ form-horizontal"/x, f) + assert_match(/input .*?type="hidden" .*?name="_method" - .*?value="(patch|put)"/x, f - assert_match /input .*?type="text" + .*?value="(patch|put)"/x, f) + assert_match(/input .*?type="text" .*?value="AAAAA" - .*?name="crud_test_model\[name\]"/x, f - assert_match /input .*?type="text" - .*?name="crud_test_model\[birthdate\]"/x, f - assert_match /input .*?type="text" + .*?name="crud_test_model\[name\]"/x, f) + assert_match(/input .*?type="text" + .*?name="crud_test_model\[birthdate\]"/x, f) + assert_match(/input .*?type="text" .*?value="9" - .*?name="crud_test_model\[children\]"/x, f - assert_match /input .*?type="checkbox" - .*?name="crud_test_model\[human\]"/x, f - assert_match /button\ .*?type="submit".*\> + .*?name="crud_test_model\[children\]"/x, f) + assert_match(/input .*?type="checkbox" + .*?name="crud_test_model\[human\]"/x, f) + assert_match(%r{button\ .*?type="submit".*> #{t('global.button.save')} - \<\/button\>/x, f + }x, f) end test 'standard form with errors' do e = crud_test_models('AAAAA') e.name = nil - assert !e.valid? + + assert_not e.valid? f = with_test_routing do capture do @@ -81,18 +84,18 @@ class FormHelperTest < ActionView::TestCase end end - assert_match /form .*?action="\/crud_test_models\/#{e.id}" - .*?method="post"/x, f - assert_match /input .*?type="hidden" + assert_match(%r{form .*?action="/crud_test_models/#{e.id}" + .*?method="post"}x, f) + assert_match(/input .*?type="hidden" .*?name="_method" - .*?value="(patch|put)"/x, f - assert_match /div[^>]* id='error_explanation'/, f - assert_match /div\ class="field_with_errors"\>.*? - \]* id='error_explanation'/, f) + assert_match(/div\ class="field_with_errors">.*? + label ' \ "
    value
    ", result.squish @@ -62,7 +64,7 @@ def format_string_size(obj) test 'labeled text empty' do result = labeled('label', '') - assert result.html_safe? + assert_predicate result, :html_safe? assert_dom_equal '
    label
    ' \ "
    #{EMPTY_STRING}
    ", result.squish @@ -71,7 +73,7 @@ def format_string_size(obj) test 'labeled text as content' do result = labeled('label', 'value ') - assert result.html_safe? + assert_predicate result, :html_safe? assert_dom_equal '
    label
    ' \ "
    value <unsafe>
    ", result.squish @@ -79,21 +81,22 @@ def format_string_size(obj) test 'labeled attr' do result = labeled_attr('foo', :size) - assert result.html_safe? + + assert_predicate result, :html_safe? assert_dom_equal '
    Size
    ' \ "
    3 chars
    ", result.squish end test 'format nil' do - assert EMPTY_STRING.html_safe? + assert_predicate EMPTY_STRING, :html_safe? assert_equal EMPTY_STRING, f(nil) end test 'format Strings' do assert_equal 'blah blah', f('blah blah') assert_equal '', f('') - assert !f('').html_safe? + assert_not f('').html_safe? end unless ENV['NON_LOCALIZED'] # localization dependent tests @@ -123,74 +126,88 @@ def format_string_size(obj) test 'format integer column' do m = crud_test_models(:AAAAA) + assert_equal '9', format_type(m, :children) m.children = 10_000 + assert_equal '10'000', format_type(m, :children) end unless ENV['NON_LOCALIZED'] # localization dependent tests test 'format float column' do m = crud_test_models(:AAAAA) + assert_equal '1.10', format_type(m, :rating) m.rating = 3.145001 # you never know with these floats.. + assert_equal '3.15', format_type(m, :rating) end test 'format decimal column' do m = crud_test_models(:AAAAA) + assert_equal '10'000'000.1111', format_type(m, :income) end test 'format date column' do m = crud_test_models(:AAAAA) + assert_equal 'Sa, 01.01.1910', format_type(m, :birthdate) end test 'format datetime column' do m = crud_test_models(:AAAAA) + assert_equal '01.01.2010 11:21', format_type(m, :last_seen) end end test 'format time column' do m = crud_test_models(:AAAAA) + assert_equal '01:01', format_type(m, :gets_up_at) end test 'format text column' do m = crud_test_models(:AAAAA) + assert_equal "

    AAAAA BBBBB CCCCC\n
    AAAAA BBBBB CCCCC\n

    ", format_type(m, :remarks) - assert format_type(m, :remarks).html_safe? + assert_predicate format_type(m, :remarks), :html_safe? end test 'format boolean false column' do m = crud_test_models(:AAAAA) m.human = false + assert_equal 'nein', format_type(m, :human) end test 'format boolean true column' do m = crud_test_models(:AAAAA) m.human = true + assert_equal 'ja', format_type(m, :human) end test 'format belongs to column without content' do m = crud_test_models(:AAAAA) + assert_equal t('global.associations.no_entry'), format_attr(m, :companion) end test 'format belongs to column with content' do m = crud_test_models(:BBBBB) + assert_equal 'AAAAA', format_attr(m, :companion) end test 'format has_many column with content' do m = crud_test_models(:CCCCC) + assert_equal '
    • AAAAA
    • BBBBB
    ', format_attr(m, :others) end @@ -199,6 +216,6 @@ def format_string_size(obj) assert_equal 'Camel Case', captionize(:camel_case) assert_equal 'All Upper Case', captionize('all upper case') assert_equal 'With Object', captionize('With object', Object.new) - assert !captionize('bad ').html_safe? + assert_not captionize('bad <title>').html_safe? end end diff --git a/test/helpers/i18n_helper_test.rb b/test/helpers/i18n_helper_test.rb index 653ff4254..ec5593be4 100644 --- a/test/helpers/i18n_helper_test.rb +++ b/test/helpers/i18n_helper_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -17,42 +19,49 @@ class I18nHelperTest < ActionView::TestCase I18n.locale, global: { test_key: 'global' } ) + assert_equal 'global', ti(:test_key) I18n.backend.store_translations( I18n.locale, list: { global: { test_key: 'list global' } } ) + assert_equal 'list global', ti(:test_key) I18n.backend.store_translations( I18n.locale, list: { index: { test_key: 'list index' } } ) + assert_equal 'list index', ti(:test_key) I18n.backend.store_translations( I18n.locale, crud: { global: { test_key: 'crud global' } } ) + assert_equal 'crud global', ti(:test_key) I18n.backend.store_translations( I18n.locale, crud: { index: { test_key: 'crud index' } } ) + assert_equal 'crud index', ti(:test_key) I18n.backend.store_translations( I18n.locale, crud_test_models: { global: { test_key: 'test global' } } ) + assert_equal 'test global', ti(:test_key) I18n.backend.store_translations( I18n.locale, crud_test_models: { index: { test_key: 'test index' } } ) + assert_equal 'test index', ti(:test_key) end @@ -63,6 +72,7 @@ class I18nHelperTest < ActionView::TestCase I18n.locale, global: { associations: { test_key: 'global' } } ) + assert_equal 'global', ta(:test_key, assoc) I18n.backend.store_translations( @@ -75,6 +85,7 @@ class I18nHelperTest < ActionView::TestCase } } ) + assert_equal 'model', ta(:test_key, assoc) I18n.backend.store_translations( @@ -91,6 +102,7 @@ class I18nHelperTest < ActionView::TestCase } } ) + assert_equal 'companion', ta(:test_key, assoc) assert_equal 'global', ta(:test_key) diff --git a/test/helpers/order_controlling_helper_test.rb b/test/helpers/order_controlling_helper_test.rb index fd7e45bdc..a5a2cc473 100644 --- a/test/helpers/order_controlling_helper_test.rb +++ b/test/helpers/order_controlling_helper_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2018, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -37,10 +39,10 @@ def setup test '#controlling_chart_datasets' do assert_equal [ - { 'label': 'Verrechenbar', 'data': [10, 10, 20], 'backgroundColor': '#69B978' }, - { 'label': 'Nicht verrechenbar', 'data': [5, 5, 8], 'backgroundColor': '#f0e54e' }, - { 'label': 'Definitiv geplant', 'data': [0, 0, 2], 'backgroundColor': '#4286e7' }, - { 'label': 'Provisorisch geplant', 'data': [0, 0, 0], 'backgroundColor': '#9bcbd4' } + { label: 'Verrechenbar', data: [10, 10, 20], backgroundColor: '#69B978' }, + { label: 'Nicht verrechenbar', data: [5, 5, 8], backgroundColor: '#f0e54e' }, + { label: 'Definitiv geplant', data: [0, 0, 2], backgroundColor: '#4286e7' }, + { label: 'Provisorisch geplant', data: [0, 0, 0], backgroundColor: '#9bcbd4' } ].to_json, controlling_chart_datasets end end diff --git a/test/helpers/table_helper_test.rb b/test/helpers/table_helper_test.rb index 80dda28f2..bed58c42c 100644 --- a/test/helpers/table_helper_test.rb +++ b/test/helpers/table_helper_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -39,27 +41,30 @@ def can?(_action, _resource) test 'empty table should render message' do result = plain_table_or_message([]) {} - assert result.html_safe? - assert_match /\<div class=["']table["']\>.*\<\/div\>/, result + + assert_predicate result, :html_safe? + assert_match(%r{<div class=["']table["']>.*</div>}, result) end test 'non empty table should render table' do - result = plain_table_or_message(%w(foo bar)) do |t| + result = plain_table_or_message(%w[foo bar]) do |t| t.attrs :size, :upcase end - assert result.html_safe? - assert_match(/^\<div class="unindented"><table.*\<\/table\><\/div>$/, result) + + assert_predicate result, :html_safe? + assert_match(%r{^<div class="unindented"><table.*</table></div>$}, result) end test 'table with attrs' do expected = DryCrud::Table::Builder.table( - %w(foo bar), self, + %w[foo bar], self, class: 'table table-striped table-hover table-condensed' ) do |t| t.attrs :size, :upcase end - actual = plain_table(%w(foo bar), :size, :upcase) - assert actual.html_safe? + actual = plain_table(%w[foo bar], :size, :upcase) + + assert_predicate actual, :html_safe? assert_equal expected, actual end @@ -98,7 +103,7 @@ def can?(_action, _resource) assert_count 7, REGEXP_ROWS, table assert_count 4, REGEXP_HEADERS, table assert_count 0, REGEXP_SORT_HEADERS, table - assert_count 6, /<span>.+?<\/span>/, table + assert_count 6, %r{<span>.+?</span>}, table end test 'custom list table with attributes and block' do @@ -113,7 +118,7 @@ def can?(_action, _resource) assert_count 7, REGEXP_ROWS, table assert_count 3, REGEXP_SORT_HEADERS, table assert_count 4, REGEXP_HEADERS, table - assert_count 6, /<span>.+?<\/span>/, table + assert_count 6, %r{<span>.+?</span>}, table end test 'standard list table with ascending sort params' do @@ -128,6 +133,7 @@ def params end sort_header_desc = %r{<th><a .*?sort_dir=desc.*?>Children</a> ↓</th>} + assert_count 7, REGEXP_ROWS, table assert_count 13, REGEXP_SORT_HEADERS, table assert_count 1, sort_header_desc, table @@ -145,6 +151,7 @@ def params end sort_header_asc = %r{<th><a .*?sort_dir=asc.*?>Children</a> ↑</th>} + assert_count 7, REGEXP_ROWS, table assert_count 13, REGEXP_SORT_HEADERS, table assert_count 1, sort_header_asc, table @@ -162,6 +169,7 @@ def params end sort_header_desc = %r{<th><a .*?sort_dir=desc.*?>Chatty</a> ↓</th>} + assert_count 7, REGEXP_ROWS, table assert_count 2, REGEXP_SORT_HEADERS, table assert_count 1, sort_header_desc, table @@ -203,7 +211,7 @@ def params assert_count 7, REGEXP_ROWS, table assert_count 6, REGEXP_HEADERS, table - assert_count 6, /<span>.+?<\/span>/m, table + assert_count 6, %r{<span>.+?</span>}m, table assert_count 12, REGEXP_ACTION_CELL, table # edit, delete links end @@ -219,7 +227,7 @@ def params assert_count 7, REGEXP_ROWS, table assert_count 3, REGEXP_SORT_HEADERS, table assert_count 6, REGEXP_HEADERS, table - assert_count 6, /<span>.+?<\/span>/m, table + assert_count 6, %r{<span>.+?</span>}m, table assert_count 12, REGEXP_ACTION_CELL, table # edit, delete links end diff --git a/test/helpers/utility_helper_test.rb b/test/helpers/utility_helper_test.rb index 5116a7ef3..f66eaa965 100644 --- a/test/helpers/utility_helper_test.rb +++ b/test/helpers/utility_helper_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -10,36 +12,42 @@ class UtilityHelperTest < ActionView::TestCase include CrudTestHelper test 'content_tag_nested escapes safe correctly' do - html = content_tag_nested(:div, %w(a b)) { |e| content_tag(:span, e) } + html = content_tag_nested(:div, %w[a b]) { |e| content_tag(:span, e) } + assert_equal '<div><span>a</span><span>b</span></div>', html end test 'content_tag_nested escapes unsafe correctly' do - html = content_tag_nested(:div, %w(a b)) { |e| "<#{e}>" } + html = content_tag_nested(:div, %w[a b]) { |e| "<#{e}>" } + assert_equal '<div><a><b></div>', html end test 'content_tag_nested without block' do - html = content_tag_nested(:div, %w(a b)) + html = content_tag_nested(:div, %w[a b]) + assert_equal '<div>ab</div>', html end test 'safe_join without block' do html = safe_join(['<a>', '<b>'.html_safe]) + assert_equal '<a><b>', html end test 'safe_join with block' do - html = safe_join(%w(a b)) { |e| content_tag(:span, e) } + html = safe_join(%w[a b]) { |e| content_tag(:span, e) } + assert_equal '<span>a</span><span>b</span>', html end test 'default attributes do not include id and password' do reset_db setup_db - assert_equal [:name, :email, :whatever, :children, :companion_id, :rating, - :income, :birthdate, :gets_up_at, :last_seen, :human, - :remarks, :created_at, :updated_at], + + assert_equal %i[name email whatever children companion_id rating + income birthdate gets_up_at last_seen human + remarks created_at updated_at], default_crud_attrs reset_db end @@ -50,6 +58,7 @@ class UtilityHelperTest < ActionView::TestCase create_test_data m = crud_test_models(:AAAAA) + assert_equal :string, column_type(m, :name) assert_equal :integer, column_type(m, :children) assert_equal :integer, column_type(m, :companion_id) diff --git a/test/helpers/worktime_helper_test.rb b/test/helpers/worktime_helper_test.rb index 6d841a4e7..807ff17a0 100644 --- a/test/helpers/worktime_helper_test.rb +++ b/test/helpers/worktime_helper_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -34,25 +36,28 @@ def setup test 'worktime account' do worktime = Absencetime.new(account_id: absences(:vacation).id) + assert_equal 'Ferien', worktime_account(worktime) end def worktime_account(worktime) - worktime.account.label_verbose if worktime.account + worktime.account&.label_verbose end test 'worktime description with ticket' do worktime = Worktime.new(description: 'desc', ticket: '123') + assert_equal '123 - desc', worktime_description(worktime) end test 'worktime description without ticket' do worktime = Worktime.new(description: 'desc') + assert_equal 'desc', worktime_description(worktime) end test 'holiday time class' do - assert_equal 'holiday', overview_day_class(@worktimes, Date.new(2014, 06, 9)) # pfingstmontag + assert_equal 'holiday', overview_day_class(@worktimes, Date.new(2014, 0o6, 9)) # pfingstmontag end test 'missing time class' do @@ -65,16 +70,19 @@ def worktime_account(worktime) test 'time range without' do worktime = Worktime.new(from_start_time: '8:00', to_end_time: '11:59') + assert_equal '08:00 - 11:59', time_range(worktime) end test 'time range without any times' do worktime = Worktime.new + assert_equal ' ', time_range(worktime) end test 'time range without end time' do worktime = Worktime.new(from_start_time: '8:00') + assert_equal '08:00 - ', time_range(worktime) end end diff --git a/test/integration/choose_order_test.rb b/test/integration/choose_order_test.rb index 6d6f47a2e..73740b759 100644 --- a/test/integration/choose_order_test.rb +++ b/test/integration/choose_order_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -31,10 +33,13 @@ class ChooseOrderTest < ActionDispatch::IntegrationTest test 'keeps current tab when changing orders' do timeout_safe do click_link 'Positionen' - assert page.has_selector?('a', text: 'Buchungsposition hinzufügen') # dummy query to wait for page load + + assert page.has_link?(href: new_order_accounting_post_path(order_id: order.id)) # query forces to wait for page load assert_equal order_accounting_posts_path(order), current_path selectize('choosable_order_id', 'Demo', term: 'demo', clear: true) + + assert page.has_link?(href: new_order_accounting_post_path(order_id: orders(:hitobito_demo).id)) # query forces to wait for page load assert_equal order_accounting_posts_path(orders(:hitobito_demo)), current_path assert page.has_selector?('li.active', text: 'Positionen') end diff --git a/test/integration/create_order_test.rb b/test/integration/create_order_test.rb index 9193803ae..1735d9f75 100644 --- a/test/integration/create_order_test.rb +++ b/test/integration/create_order_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -13,9 +15,11 @@ class CreateOrderTest < ActionDispatch::IntegrationTest test 'create order with existing client, without category' do timeout_safe do click_add_contact # disabled + assert page.has_no_selector?('#order_order_contacts_attributes_0_contact_id_or_crm') click_link('category_work_item_id_create_link') # disabled + assert page.has_no_selector?('#work_item_name') selectize('client_work_item_id', 'Swisstopo') @@ -28,6 +32,7 @@ class CreateOrderTest < ActionDispatch::IntegrationTest assert_creatable order = WorkItem.where(name: 'New Order').first + assert_equal clients(:swisstopo).work_item_id, order.parent_id assert_equal [contacts(:swisstopo_2)], order.order.contacts end @@ -42,6 +47,7 @@ class CreateOrderTest < ActionDispatch::IntegrationTest assert_creatable client = WorkItem.where(name: 'New Client').first order = WorkItem.where(name: 'New Order').first + assert_equal client.id, order.parent_id end end @@ -55,9 +61,10 @@ class CreateOrderTest < ActionDispatch::IntegrationTest fill_mandatory_fields assert_creatable - client = clients(:puzzle) + clients(:puzzle) category = work_items(:intern) order = WorkItem.where(name: 'New Order').first + assert_equal category.id, order.parent_id end end @@ -74,6 +81,7 @@ class CreateOrderTest < ActionDispatch::IntegrationTest assert_creatable client = clients(:puzzle) order = WorkItem.where(name: 'New Order').first + assert_equal client.work_item_id, order.parent_id end end @@ -90,6 +98,7 @@ class CreateOrderTest < ActionDispatch::IntegrationTest client = clients(:puzzle) category = WorkItem.where(name: 'New Category').first order = WorkItem.where(name: 'New Order').first + assert_equal client.id, category.parent_id assert_equal category.id, order.parent_id end @@ -107,6 +116,7 @@ class CreateOrderTest < ActionDispatch::IntegrationTest client = WorkItem.where(name: 'New Client').first category = WorkItem.where(name: 'New Category').first order = WorkItem.where(name: 'New Order').first + assert_equal client.id, category.parent_id assert_equal category.id, order.parent_id end @@ -125,6 +135,7 @@ class CreateOrderTest < ActionDispatch::IntegrationTest client = WorkItem.where(name: 'New Client').first category = WorkItem.where(name: 'New Category').first order = WorkItem.where(name: 'New Order').first + assert_equal client.id, category.parent_id assert_equal client.id, order.parent_id end @@ -137,9 +148,11 @@ class CreateOrderTest < ActionDispatch::IntegrationTest element = find('#category_work_item_id + .selectize-control') element.find('.selectize-input').click # open dropdown options = element.find('.selectize-dropdown-content') + assert options.has_selector?('div', count: 2) selectize('client_work_item_id', 'Swisstopo') - assert !options.has_selector?('div') + + assert_not options.has_selector?('div') end end @@ -148,19 +161,24 @@ class CreateOrderTest < ActionDispatch::IntegrationTest selectize('client_work_item_id', 'Puzzle') check('category_active') click_link('category_work_item_id_create_link') - click_link('Abbrechen') + within('.modal-dialog') do + click_link('Abbrechen') + end selectize('client_work_item_id', 'Swisstopo') click_link('category_work_item_id_create_link') - fill_in('work_item_name', with: 'New Category') - fill_in('work_item_shortname', with: 'NECA') - click_button 'Speichern' - - assert find('#category_work_item_id + .selectize-control'). - has_selector?('.selectize-input .item', text: 'New Category') + within('.modal-dialog') do + fill_in('work_item_name', with: 'New Category') + fill_in('work_item_shortname', with: 'NECA') + click_button 'Speichern' + end + + assert find('#category_work_item_id + .selectize-control') + .has_selector?('.selectize-input .item', text: 'New Category') # sleep 0.2 id = find('#category_work_item_id', visible: false)['value'] category = WorkItem.find(id) + assert_equal 'New Category', category.name assert_equal work_items(:swisstopo).id, category.parent_id end @@ -177,6 +195,7 @@ class CreateOrderTest < ActionDispatch::IntegrationTest assert_creatable order = WorkItem.where(name: 'New Order').first + assert_equal work_items(:swisstopo).id, order.parent_id end end @@ -242,9 +261,11 @@ class CreateOrderTest < ActionDispatch::IntegrationTest fill_in('order_crm_key', with: '123') click_link('Übernehmen') - assert_equal 'New Client', find('#client_work_item_attributes_name')['value'] - fill_in('client_work_item_attributes_shortname', with: 'NECL') - click_button 'Speichern' + within('.modal-dialog') do + assert_equal 'New Client', find('#client_work_item_attributes_name')['value'] + fill_in('client_work_item_attributes_shortname', with: 'NECL') + click_button 'Speichern' + end assert_equal 'New Order', find('#order_work_item_attributes_name')['value'] @@ -256,8 +277,10 @@ class CreateOrderTest < ActionDispatch::IntegrationTest assert_creatable client = WorkItem.where(name: 'New Client').first order = WorkItem.where(name: 'New Order').first + assert_equal client.id, order.parent_id - contact = Contact.find_by_lastname('Nader') + contact = Contact.find_by(lastname: 'Nader') + assert_equal '456', contact.crm_key assert_equal client.client, contact.client assert_equal [contact], order.order.contacts @@ -280,8 +303,10 @@ class CreateOrderTest < ActionDispatch::IntegrationTest click_link('Übernehmen') assert_equal 'New Client', find('#client_work_item_attributes_name')['value'] - fill_in('client_work_item_attributes_shortname', with: 'NECL') - click_button 'Speichern' + within('.modal-dialog') do + fill_in('client_work_item_attributes_shortname', with: 'NECL') + click_button 'Speichern' + end assert_equal 'New Order', find('#order_work_item_attributes_name')['value'] @@ -293,6 +318,7 @@ class CreateOrderTest < ActionDispatch::IntegrationTest client = WorkItem.where(name: 'New Client').first category = WorkItem.where(name: 'New Category').first order = WorkItem.where(name: 'New Order').first + assert_equal client.id, category.parent_id assert_equal category.id, order.parent_id end @@ -330,8 +356,9 @@ class CreateOrderTest < ActionDispatch::IntegrationTest assert_creatable order = WorkItem.where(name: 'New Order').first + assert_equal clients(:swisstopo).work_item_id, order.parent_id - assert_equal [Contact.find_by_lastname('Nader')], order.order.contacts + assert_equal [Contact.find_by(lastname: 'Nader')], order.order.contacts end end @@ -359,6 +386,7 @@ class CreateOrderTest < ActionDispatch::IntegrationTest assert_creatable order = WorkItem.where(name: 'New Order').first category = WorkItem.where(name: 'New Category').first + assert_equal client.work_item_id, category.parent_id assert_equal category.id, order.parent_id end @@ -388,6 +416,7 @@ class CreateOrderTest < ActionDispatch::IntegrationTest assert_creatable order = WorkItem.where(name: 'New Order').first category = work_items(:intern) + assert_equal category.id, order.parent_id end end @@ -437,12 +466,15 @@ class CreateOrderTest < ActionDispatch::IntegrationTest assert has_unchecked_field?('category_active') selecti0 = find('#order_order_contacts_attributes_0_contact_id_or_crm + .selectize-control') + assert selecti0.has_selector?('.selectize-input .item', text: 'Hauswart Hans') selecti0.find('.selectize-input').click # populate & open dropdown + assert selecti0.has_selector?('.selectize-dropdown-content .option', count: 3) find('body').send_keys(:escape) # close dropdown selecti1 = find('#order_order_contacts_attributes_1_contact_id_or_crm + .selectize-control') + assert selecti1.has_selector?('.selectize-input .item', text: 'Nader Fred') click_add_contact @@ -481,7 +513,7 @@ class CreateOrderTest < ActionDispatch::IntegrationTest fill_in('order_crm_key', with: '123') click_link('Übernehmen') - assert_match /bereits erfasst/, find('#crm_key').text + assert_match(/bereits erfasst/, find('#crm_key').text) end end @@ -496,7 +528,7 @@ class CreateOrderTest < ActionDispatch::IntegrationTest fill_in('order_crm_key', with: '123') click_link('Übernehmen') - assert_match /crm error occurred/, find('#crm_key .help-block').text + assert_match(/crm error occurred/, find('#crm_key .help-block').text) assert_equal '', find('#client_work_item_id', visible: false)['value'] end end @@ -508,11 +540,15 @@ class CreateOrderTest < ActionDispatch::IntegrationTest assert_no_selector("a[data-object-class='order-team-member'].remove_nested_fields_link") find("a[data-object-class='order_team_member'].add_nested_fields_link").click - assert find_field('order_order_team_members_attributes_0_employee_id', visible: false)[:class].include?('selectized') + + assert_includes find_field('order_order_team_members_attributes_0_employee_id', + visible: false)[:class], 'selectized' assert_selector("a[data-object-class='order_team_member'].remove_nested_fields_link", count: 1) find("a[data-object-class='order_team_member'].add_nested_fields_link").click - assert find_field('order_order_team_members_attributes_1_employee_id', visible: false)[:class].include?('selectized') + + assert_includes find_field('order_order_team_members_attributes_1_employee_id', + visible: false)[:class], 'selectized' assert_selector("a[data-object-class='order_team_member'].remove_nested_fields_link", count: 2) end @@ -520,20 +556,24 @@ class CreateOrderTest < ActionDispatch::IntegrationTest def create_client click_link('client_work_item_id_create_link') - fill_in('client_work_item_attributes_name', with: 'New Client') - fill_in('client_work_item_attributes_shortname', with: 'NECL') - click_button 'Speichern' + within('.modal-dialog') do + fill_in('client_work_item_attributes_name', with: 'New Client') + fill_in('client_work_item_attributes_shortname', with: 'NECL') + click_button 'Speichern' + end end def create_category click_link('category_work_item_id_create_link') - fill_in('work_item_name', with: 'New Category') - fill_in('work_item_shortname', with: 'NECA') - click_button 'Speichern' + within('.modal-dialog') do + fill_in('work_item_name', with: 'New Category') + fill_in('work_item_shortname', with: 'NECA') + click_button 'Speichern' + end end def click_add_contact - find("a.add_nested_fields_link[data-object-class='order_contact']").click + find("a.add_nested_fields_link[data-object-class='order_contact']").trigger('click') end def fill_mandatory_fields(with_name = true) @@ -546,6 +586,7 @@ def fill_mandatory_fields(with_name = true) def assert_creatable click_button 'Speichern' + assert has_content?('New Order wurde erfolgreich erstellt') end diff --git a/test/integration/create_ordertime_test.rb b/test/integration/create_ordertime_test.rb index a42adfe3a..2971573e1 100644 --- a/test/integration/create_ordertime_test.rb +++ b/test/integration/create_ordertime_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -16,6 +18,7 @@ class CreateOrdertimeTest < ActionDispatch::IntegrationTest assert_equal '/ordertimes', current_path time = Worktime.order(:id).last + assert_equal work_items(:hitobito_demo_site), time.account end end @@ -30,22 +33,27 @@ class CreateOrdertimeTest < ActionDispatch::IntegrationTest assert page.has_selector?('#error_explanation') item = work_items(:hitobito_demo_site) + assert_equal item.id.to_s, find('#ordertime_account_id', visible: false).value element = find('#ordertime_account_id + .selectize-control') + assert_equal item.label_verbose, element.find('.selectize-input div').text end end test 'create ordertime select accounting_post with billable=true checks billable checkbox' do find('#ordertime_billable').set(false) + assert_not find('#ordertime_billable').checked? selectize('ordertime_account_id', 'Webauftritt', term: 'web') - assert find('#ordertime_billable').checked? + + assert_predicate find('#ordertime_billable'), :checked? end test 'create ordertime select accounting_post with billable=false unchecks billable checkbox' do - assert find('#ordertime_billable').checked? + assert_predicate find('#ordertime_billable'), :checked? selectize('ordertime_account_id', 'PuzzleTime', term: 'time') + assert_not find('#ordertime_billable').checked? end diff --git a/test/integration/edit_accounting_post_test.rb b/test/integration/edit_accounting_post_test.rb index 473cfc8e2..35cf0742f 100644 --- a/test/integration/edit_accounting_post_test.rb +++ b/test/integration/edit_accounting_post_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -19,17 +21,20 @@ class EditAccountingPostTest < ActionDispatch::IntegrationTest fill_in('accounting_post_offered_hours', with: 200.5) fill_in('accounting_post_offered_rate', with: 7.4) + assert_equal 200.5 / must_hours_per_day, find_field('accounting_post_offered_days').value.to_f assert_equal 200.5 * 7.4, find_field('accounting_post_offered_total').value.to_f page.find('body').click # otherwise capybara will be too fast after the change event fill_in('accounting_post_offered_days', with: 77) + assert_equal 77 * must_hours_per_day, find_field('accounting_post_offered_hours').value.to_f - assert_equal 7.4, find_field('accounting_post_offered_rate').value.to_f + assert_in_delta(7.4, find_field('accounting_post_offered_rate').value.to_f) assert_equal 77 * must_hours_per_day * 7.4, find_field('accounting_post_offered_total').value.to_f page.find('body').click fill_in('accounting_post_offered_rate', with: 1000) + assert_equal 77 * must_hours_per_day, find_field('accounting_post_offered_hours').value.to_f assert_equal 77, find_field('accounting_post_offered_days').value.to_f assert_equal 77 * must_hours_per_day * 1000, find_field('accounting_post_offered_total').value.to_f @@ -37,6 +42,7 @@ class EditAccountingPostTest < ActionDispatch::IntegrationTest page.find('body').click fill_in('accounting_post_offered_total', with: 1234.01) page.find('body').click + assert_equal '1000', find_field('accounting_post_offered_rate').value assert_equal 1234.01 / 1000, find_field('accounting_post_offered_hours').value.to_f assert_equal 1234.01 / 1000 / must_hours_per_day, find_field('accounting_post_offered_days').value.to_f diff --git a/test/integration/edit_contract_test.rb b/test/integration/edit_contract_test.rb index d16ec672d..72f07b8d8 100644 --- a/test/integration/edit_contract_test.rb +++ b/test/integration/edit_contract_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/test/integration/edit_order_test.rb b/test/integration/edit_order_test.rb index b3a449819..b1ed817af 100644 --- a/test/integration/edit_order_test.rb +++ b/test/integration/edit_order_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -15,16 +17,21 @@ class EditOrderTest < ActionDispatch::IntegrationTest assert_no_selector("a[data-object-class='order-contact'].remove_nested_fields_link") click_add_contact - assert find_field('order_order_contacts_attributes_0_contact_id_or_crm', visible: false)[:class].include?('selectized') + + assert_includes find_field('order_order_contacts_attributes_0_contact_id_or_crm', + visible: false)[:class], 'selectized' assert_selector("a[data-object-class='order_contact'].remove_nested_fields_link", count: 1) click_add_contact - assert find_field('order_order_contacts_attributes_1_contact_id_or_crm', visible: false)[:class].include?('selectized') + + assert_includes find_field('order_order_contacts_attributes_1_contact_id_or_crm', + visible: false)[:class], 'selectized' assert_selector("a[data-object-class='order_contact'].remove_nested_fields_link", count: 2) end test 'EDIT without crm, contacts are populated according to client' do click_add_contact + assert open_selectize('order_order_contacts_attributes_0_contact_id_or_crm') .assert_selector('.option', count: 2) end @@ -41,6 +48,7 @@ class EditOrderTest < ActionDispatch::IntegrationTest setup_crm_contacts visit edit_order_path(order) click_add_contact + assert open_selectize('order_order_contacts_attributes_0_contact_id_or_crm') .assert_selector('.option', count: 3) end @@ -55,12 +63,14 @@ class EditOrderTest < ActionDispatch::IntegrationTest test 'order with worktimes has disabled destroy link' do visit edit_order_path(order) + assert find('a.disabled', text: 'Löschen') assert has_no_link?('Löschen', href: order_path(order)) end test 'order without worktimes has active destroy link' do visit edit_order_path(order_without_worktimes) + assert has_link?('Löschen', href: order_path(order_without_worktimes)) assert_no_selector('a.disabled', text: 'Löschen') end @@ -69,6 +79,7 @@ class EditOrderTest < ActionDispatch::IntegrationTest def click_add_contact find("a.add_nested_fields_link[data-object-class='order_contact']").click + page.assert_selector('#order_order_contacts_attributes_0_contact_id_or_crm', visible: false) end diff --git a/test/integration/edit_other_ordertime_test.rb b/test/integration/edit_other_ordertime_test.rb index 1e9e887d7..b758673b6 100644 --- a/test/integration/edit_other_ordertime_test.rb +++ b/test/integration/edit_other_ordertime_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -17,33 +19,38 @@ class EditOtherOrdertimeTest < ActionDispatch::IntegrationTest ordertime = create_ordertime put ordertime_path(ordertime), params: { ordertime: { hours: '8:30' } } follow_redirect! + assert_response :success assert_equal '/evaluator/details', path assert_equal 'Alle Arbeitszeiten wurden erfasst', flash[:notice] - assert_equal 8.5, ordertime.hours + assert_in_delta(8.5, ordertime.hours) end test 'update ordertime with more hours' do ordertime = create_ordertime put ordertime_path(ordertime), params: { ordertime: { hours: '9:30' } } + assert_response :success assert_equal ordertime_path(ordertime), path assert_match(/Die gesamte Anzahl Stunden kann nicht vergrössert werden/, response.body) ordertime.reload - assert_equal 8.5, ordertime.hours + + assert_in_delta(8.5, ordertime.hours) end test 'update ordertime with less hours' do ordertime = create_ordertime put ordertime_path(ordertime), params: { ordertime: { hours: '7:30' } } follow_redirect! + assert_response :success assert_equal split_ordertimes_path, path assert_match(/Die Zeiten wurden noch nicht gespeichert/, response.body) assert_match(/Bitte schliessen sie dazu den Aufteilungsprozess ab/, response.body) ordertime.reload + assert_equal ordertime, Ordertime.last # splitted times will be persisted later as new records - assert_equal 8.5, ordertime.hours + assert_in_delta(8.5, ordertime.hours) post create_part_ordertimes_path, params: { @@ -55,11 +62,12 @@ class EditOtherOrdertimeTest < ActionDispatch::IntegrationTest } } follow_redirect! + assert_response :success assert_equal '/evaluator/details', path assert_match(/Alle Arbeitszeiten wurden erfasst/, response.body) - assert_equal 7.5, Ordertime.order(id: :desc).limit(2).second.hours - assert_equal 1.0, Ordertime.order(id: :desc).limit(2).first.hours + assert_in_delta(7.5, Ordertime.order(id: :desc).limit(2).second.hours) + assert_in_delta(1.0, Ordertime.order(id: :desc).limit(2).first.hours) end private @@ -76,10 +84,12 @@ def create_ordertime } } follow_redirect! + assert_response :success assert_equal '/evaluator/details', path ordertime = Ordertime.last - assert_equal 8.5, ordertime.hours + + assert_in_delta(8.5, ordertime.hours) ordertime end diff --git a/test/integration/edit_worktimes_as_order_responsible.rb b/test/integration/edit_worktimes_as_order_responsible.rb index 4afa5f1e9..3e256e6a6 100644 --- a/test/integration/edit_worktimes_as_order_responsible.rb +++ b/test/integration/edit_worktimes_as_order_responsible.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -9,21 +11,21 @@ class EditWorktimesAsOrderResponsibleTest < ActionDispatch::IntegrationTest setup :login test 'can change own committed worktimes on own order' do - find('a.add-other').click - assert_selector('.btn-primary') - + click_link 'Zeiten freigeben' click_button 'Speichern' - assert_no_selector('.btn-primary') visit('/ordertimes/10/edit') + assert_selector('form[action="/ordertimes/10"]') click_button 'Speichern' + assert_no_selector('.alert.alert-danger') assert_selector('.alert.alert-success') end def login login_as(:lucien) + visit('/ordertimes') end end diff --git a/test/integration/edit_worktimes_commit_test.rb b/test/integration/edit_worktimes_commit_test.rb index 2615b3e54..4b89443a5 100644 --- a/test/integration/edit_worktimes_commit_test.rb +++ b/test/integration/edit_worktimes_commit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -11,9 +13,11 @@ class EditWorktimesCommitTest < ActionDispatch::IntegrationTest login current_month = I18n.l(Time.zone.today.at_end_of_month, format: :month) label = find("#committed_worktimes_at_#{employees(:long_time_john).id}") + assert label.has_selector?('.icon-square.red') find("a[data-element='#committed_worktimes_at_#{employees(:long_time_john).id}']").click + assert_selector('.modal-body #employee_committed_worktimes_at') select(current_month, from: 'employee_committed_worktimes_at') click_button 'Speichern' diff --git a/test/integration/employees/new_session_test.rb b/test/integration/employees/new_session_test.rb new file mode 100644 index 000000000..ae8b223e3 --- /dev/null +++ b/test/integration/employees/new_session_test.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +# Copyright (c) 2006-2022, Puzzle ITC GmbH. This file is part of +# PuzzleTime and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/puzzle/puzzletime. + +require 'test_helper' + +module Employees + class NewSessionTest < ActionDispatch::IntegrationTest + def setup + # We use the rack_test driver as this one does not evaluate javascript. + # This is required as we want to test if the page contains the necessary class attribute and javascript snippet + # to execute the auto login. For this the auto login redirect can't actually happen. + Capybara.current_driver = :rack_test + end + + def teardown + # Let's restore the original driver. + Capybara.use_default_driver + end + + test 'login button has auto-login class if eligible' do + Settings.auth.db.active = false + Settings.auth.omniauth.keycloakopenid.active = true + Settings.auth.omniauth.saml.active = false + + visit new_employee_session_path + + assert_selector 'a.auto-login', text: 'Mit Puzzle SSO anmelden' + end + + test 'login button does not have auto-login class if uneligible' do + Settings.auth.db.active = true + Settings.auth.omniauth.keycloakopenid.active = true + Settings.auth.omniauth.saml.active = false + + visit new_employee_session_path + + assert_selector 'a', text: 'Mit Puzzle SSO anmelden' + assert_no_selector 'a.auto-login', text: 'Mit Puzzle SSO anmelden' + end + + test 'page includes auto-login javascript if eligible' do + Settings.auth.db.active = false + Settings.auth.omniauth.keycloakopenid.active = true + Settings.auth.omniauth.saml.active = false + + visit new_employee_session_path + + assert_includes page.text(:all), "$('.auto-login').click()" + end + + test 'page excludes auto-login javascript if uneligible' do + Settings.auth.db.active = true + Settings.auth.omniauth.keycloakopenid.active = true + Settings.auth.omniauth.saml.active = false + + visit new_employee_session_path + + assert page.text(:all).exclude? "$('.auto-login').click()" + end + end +end diff --git a/test/integration/invoice_form_test.rb b/test/integration/invoice_form_test.rb index 4d3d3bc42..6498c5b1d 100644 --- a/test/integration/invoice_form_test.rb +++ b/test/integration/invoice_form_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -11,47 +13,63 @@ class NewInvoiceTest < ActionDispatch::IntegrationTest test 'without params has defaults' do assert_equal '', find_field('invoice_period_from').value assert_equal '', find_field('invoice_period_to').value - refute find_field('manual_invoice').checked? + assert_not_predicate find_field('manual_invoice'), :checked? assert_checkboxes(all("input[name='invoice[employee_ids][]']"), order_employees) assert_checkboxes(all("input[name='invoice[work_item_ids][]']"), order_work_items) assert_equal I18n.l(Time.zone.today + order.contract.payment_period.days), find_field('invoice_due_date').value - assert find_field("invoice_billing_address_id_#{order.default_billing_address_id}").checked? + assert_predicate find_field("invoice_billing_address_id_#{order.default_billing_address_id}"), :checked? end test 'click on manual toggles invoice filters visibility' do manual_checkbox = find_field('manual_invoice') - refute manual_checkbox.checked? + + assert_not_predicate manual_checkbox, :checked? affected_selectors = [ "input[name='invoice[employee_ids][]']", "input[name='invoice[work_item_ids][]']", "input[name='invoice[grouping]']" ] - assert affected_selectors.all? { |selector| all(selector).present? } + + assert(affected_selectors.all? { |selector| all(selector).present? }) manual_checkbox.click - assert affected_selectors.none? { |selector| all(selector).present? } + + assert(affected_selectors.none? { |selector| all(selector).present? }) manual_checkbox.click - assert affected_selectors.all? { |selector| all(selector).present? } + + assert(affected_selectors.all? { |selector| all(selector).present? }) end test 'sets calculated total on page load' do expected_total = '%.2f' % (billable_hours * rate).round(2) - assert_match expected_total, find('#invoice_total_amount').text.delete("'") + + text_on_page = + find('#invoice_total_amount') + .text + .delete("'") + .delete(''') + + assert_match expected_total, text_on_page end test 'lists only employees with ordertimes on page load' do all(:name, 'invoice[employee_ids][]').map(&:value) - assert_arrays_match employees(:mark, :lucien).map(&:id).map(&:to_s), all(:name, 'invoice[employee_ids][]').map(&:value) + + assert_arrays_match employees(:mark, :lucien).map { |e| e.id.to_s }, + all(:name, 'invoice[employee_ids][]').map(&:value) reload(invoice: { period_to: '8.12.2006' }) + assert_arrays_match [employees(:mark).id.to_s], all(:name, 'invoice[employee_ids][]').map(&:value) reload(invoice: { period_from: '12.12.2006' }) + assert_arrays_match [employees(:lucien).id.to_s], all(:name, 'invoice[employee_ids][]').map(&:value) reload(invoice: { period_from: '09.12.2006', period_to: '11.12.2006' }) + assert_empty all(:name, 'invoice[employee_ids][]') end @@ -60,7 +78,8 @@ class NewInvoiceTest < ActionDispatch::IntegrationTest work_items = Fabricate.times(2, :work_item, parent: order.work_item) work_items.each { |w| Fabricate(:accounting_post, work_item: w) } - from, to = Date.parse('09.12.2006'), Date.parse('10.12.2006') + from = Date.parse('09.12.2006') + to = Date.parse('10.12.2006') (from..to).each_with_index do |date, index| Fabricate(:ordertime, @@ -69,22 +88,28 @@ class NewInvoiceTest < ActionDispatch::IntegrationTest employee: employees(:pascal)) end - reload(order: order) + reload(order:) + assert_arrays_match work_items.map { |w| w.id.to_s }, all(:name, 'invoice[work_item_ids][]').map(&:value) - reload(order: order, invoice: { period_from: '11.12.2006' }) + reload(order:, invoice: { period_from: '11.12.2006' }) + assert_empty all(:name, 'invoice[work_item_ids][]').map(&:value) - reload(order: order, invoice: { period_to: '08.12.2006' }) + reload(order:, invoice: { period_to: '08.12.2006' }) + assert_empty all(:name, 'invoice[work_item_ids][]').map(&:value) - reload(order: order, invoice: { period_from: '10.12.2006' }) + reload(order:, invoice: { period_from: '10.12.2006' }) + assert_arrays_match [work_items.last.id.to_s], all(:name, 'invoice[work_item_ids][]').map(&:value) - reload(order: order, invoice: { period_to: '09.12.2006' }) + reload(order:, invoice: { period_to: '09.12.2006' }) + assert_arrays_match [work_items.first.id.to_s], all(:name, 'invoice[work_item_ids][]').map(&:value) - reload(order: order, invoice: { period_from: '09.12.2006', period_to: '10.12.2006' }) + reload(order:, invoice: { period_from: '09.12.2006', period_to: '10.12.2006' }) + assert_arrays_match work_items.map { |w| w.id.to_s }, all(:name, 'invoice[work_item_ids][]').map(&:value) end @@ -97,42 +122,49 @@ class NewInvoiceTest < ActionDispatch::IntegrationTest test 'set from date updates employee checkboxes' do # check precondition - assert has_css?("#employee_checkboxes", text: "Waber Mark") + assert has_css?('#employee_checkboxes', text: 'Waber Mark') # set date, assert change_date('invoice_period_from', '09.12.2006') - refute has_css?("#employee_checkboxes", text: "Waber Mark") + + assert_not has_css?('#employee_checkboxes', text: 'Waber Mark') change_date('invoice_period_from', '08.12.2006') - assert has_css?("#employee_checkboxes", text: "Waber Mark") + + assert has_css?('#employee_checkboxes', text: 'Waber Mark') end test 'set to date updates employee checkboxes' do # check precondition - assert has_css?("#employee_checkboxes", text: "Waber Mark") + assert has_css?('#employee_checkboxes', text: 'Waber Mark') # set date, assert change_date('invoice_period_to', '07.12.2006') - refute has_css?("#employee_checkboxes", text: "Waber Mark") + + assert_not has_css?('#employee_checkboxes', text: 'Waber Mark') change_date('invoice_period_to', '08.12.2006') - assert has_css?("#employee_checkboxes", text: "Waber Mark") + + assert has_css?('#employee_checkboxes', text: 'Waber Mark') end test 'set to date updates work_items checkboxes' do # check precondition - assert has_css?("#work_item_checkboxes", text: "STOP-WEB: Webauftritt") + assert has_css?('#work_item_checkboxes', text: 'STOP-WEB: Webauftritt') # set date, assert change_date('invoice_period_to', '07.12.2006') - refute has_css?("#work_item_checkboxes", text: "STOP-WEB: Webauftritt") + + assert_not has_css?('#work_item_checkboxes', text: 'STOP-WEB: Webauftritt') change_date('invoice_period_to', '08.12.2006') - assert has_css?("#work_item_checkboxes", text: "STOP-WEB: Webauftritt") + + assert has_css?('#work_item_checkboxes', text: 'STOP-WEB: Webauftritt') end test 'change of billing client changes billing addresses' do selectize('invoice_billing_client_id', 'Puzzle') + assert find('#billing_addresses').has_content?('Eigerplatz') end @@ -169,7 +201,7 @@ def reload(params = {}) def change_date(label, date_string) page.find("##{label}").click fill_in(label, with: date_string) - page.find("#ui-datepicker-div .ui-datepicker-current-day a").click + page.find('#ui-datepicker-div .ui-datepicker-current-day a').click sleep(0.5) # give the xhr request some time to complete end diff --git a/test/integration/list_invoices_test.rb b/test/integration/list_invoices_test.rb index 5e21ed0de..b6fd1f951 100644 --- a/test/integration/list_invoices_test.rb +++ b/test/integration/list_invoices_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -5,10 +7,11 @@ require 'test_helper' -class ListOrdersTest < ActionDispatch::IntegrationTest +class ListInvoicesTest < ActionDispatch::IntegrationTest test 'list invoices as employee has no create/edit/destroy links' do timeout_safe do list_invoices_as :pascal + assert has_no_link?('Erstellen') assert has_no_link?('Bearbeiten') assert has_no_link?('Löschen') @@ -18,6 +21,7 @@ class ListOrdersTest < ActionDispatch::IntegrationTest test 'list invoices as order responsible member has create/edit/destroy links' do timeout_safe do list_invoices_as :long_time_john + assert has_link?('Erstellen') assert has_link?('Bearbeiten') assert has_link?('Löschen') @@ -27,6 +31,7 @@ class ListOrdersTest < ActionDispatch::IntegrationTest test 'list invoices as management has create/edit/destroy links' do timeout_safe do list_invoices_as :mark + assert has_link?('Erstellen') assert has_link?('Bearbeiten') assert has_link?('Löschen') diff --git a/test/integration/list_orders_test.rb b/test/integration/list_orders_test.rb index 18b2497cb..d8f0dcd25 100644 --- a/test/integration/list_orders_test.rb +++ b/test/integration/list_orders_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -9,6 +11,7 @@ class ListOrdersTest < ActionDispatch::IntegrationTest test 'list orders as employee has no create link' do timeout_safe do list_orders_as :pascal + assert has_no_link?('Erstellen') end end @@ -16,6 +19,7 @@ class ListOrdersTest < ActionDispatch::IntegrationTest test 'list orders as order responsible member has create link' do timeout_safe do list_orders_as :lucien + assert has_link?('Erstellen') end end @@ -23,6 +27,7 @@ class ListOrdersTest < ActionDispatch::IntegrationTest test 'list orders as management has create link' do timeout_safe do list_orders_as :mark + assert has_link?('Erstellen') end end @@ -30,11 +35,14 @@ class ListOrdersTest < ActionDispatch::IntegrationTest test 'list orders filters list by name' do timeout_safe do list_orders_as :mark + assert page.has_selector?('table.orders-list tbody tr', count: 3) fill_in 'Name', with: 'swiss' page.find('input#q').native.send_keys(:enter) + assert page.has_selector?('table.orders-list tbody tr', count: 1) find('.has-clear [data-clear]').click + assert page.has_selector?('table.orders-list tbody tr', count: 3) end end diff --git a/test/integration/multi_edit_worktimes.rb b/test/integration/multi_edit_worktimes.rb index d05b3c327..f2db0017b 100644 --- a/test/integration/multi_edit_worktimes.rb +++ b/test/integration/multi_edit_worktimes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -12,7 +14,8 @@ class MultiEditWorktimes < ActionDispatch::IntegrationTest find(:css, "#worktime_ids_[value='2']").set(true) find(:css, "#worktime_ids_[value='10']").set(true) click_link('Auswahl bearbeiten') + assert page.has_text?('2 Zeiten bearbeiten') - assert_equal all('#worktime_ids_', visible: false).map(&:value), %w(2 10) + assert_equal %w[2 10], all('#worktime_ids_', visible: false).map(&:value) end end diff --git a/test/integration/order_reports_test.rb b/test/integration/order_reports_test.rb index 8f464d20d..265cc48c4 100644 --- a/test/integration/order_reports_test.rb +++ b/test/integration/order_reports_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -14,10 +16,12 @@ class OrderReportsTest < ActionDispatch::IntegrationTest fill_in('start_date', with: '1.11.2006') fill_in('end_date', with: ' ') # required to lose focus on start_date + assert_selector('table.orders-report tbody tr', count: 4) fill_in('end_date', with: '1.12.2006') fill_in('start_date', with: '2.11.2006') # required to lose focus on end_date + assert_selector('table.orders-report tbody tr', count: 2) end end @@ -29,10 +33,12 @@ class OrderReportsTest < ActionDispatch::IntegrationTest fill_in('start_date', with: '1.11.2006') fill_in('end_date', with: '1.10.2006') select('Rot', from: 'target') # required to lose focus on end_date + assert_selector('#flash .alert-danger') fill_in('end_date', with: '1.12.2006') select('Orange', from: 'target') # required to lose focus on end_date + assert_no_selector('#flash .alert-danger') end end @@ -41,10 +47,12 @@ class OrderReportsTest < ActionDispatch::IntegrationTest timeout_safe do list_orders - element = find("#category_work_item_id + .selectize-control") + element = find('#category_work_item_id + .selectize-control') + element.assert_no_selector('.selectize-dropdown-content .option', visible: false) selectize('client_work_item_id', 'Puzzle') + element.assert_selector('.selectize-dropdown-content .option', count: 2, visible: false) end end diff --git a/test/integration/plannings_employees_test.rb b/test/integration/plannings_employees_test.rb index 13875c789..ffaf1b1e7 100644 --- a/test/integration/plannings_employees_test.rb +++ b/test/integration/plannings_employees_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -20,6 +22,7 @@ class PlanningsEmployeesTest < ActionDispatch::IntegrationTest find('.add').click selectize('add_work_item_select_id', 'PITC-PT: PuzzleTime', no_click: true) + page.assert_selector(row_selector, text: 'PITC-PT: PuzzleTime') row.all('.day')[0].assert_text('') @@ -43,6 +46,7 @@ class PlanningsEmployeesTest < ActionDispatch::IntegrationTest assert_percents ['50', '50', '50', '50', '50', '', '', ''], row drag(row.all('.day')[3], row.all('.day')[6]) + page.assert_selector('.planning-calendar .-selected', count: 4) page.assert_selector('.planning-panel', visible: true) diff --git a/test/integration/plannings_orders_test.rb b/test/integration/plannings_orders_test.rb index c93f7d748..50cdd7fc9 100644 --- a/test/integration/plannings_orders_test.rb +++ b/test/integration/plannings_orders_test.rb @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# frozen_string_literal: true # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 @@ -12,50 +12,58 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest test 'close panel on cancel' do row_mark.all('.day')[0].click + page.assert_selector('.planning-panel', visible: true) within '.planning-panel' do click_button 'Abbrechen' end + page.assert_selector('.planning-panel', visible: false) end test 'close panel on click outside' do row_mark.all('.day')[0].click + page.assert_selector('.planning-panel', visible: true) find('.navbar-brand').click + page.assert_selector('.planning-panel', visible: false) end test 'close panel on escape' do row_mark.all('.day')[0].click + page.assert_selector('.planning-panel', visible: true) find('body').send_keys :escape find('body').click + page.assert_selector('.planning-panel', visible: false) end test 'form values' do date = Time.zone.today.beginning_of_week Planning.create!({ employee_id: employees(:pascal).id, - work_item_id: work_item_id, - date: (date + 1.days).strftime('%Y-%m-%d'), + work_item_id:, + date: (date + 1.day).strftime('%Y-%m-%d'), percent: 25, definitive: true }) Planning.create!({ employee_id: employees(:pascal).id, - work_item_id: work_item_id, + work_item_id:, date: (date + 2.days).strftime('%Y-%m-%d'), percent: 25, definitive: false }) Planning.create!({ employee_id: employees(:pascal).id, - work_item_id: work_item_id, + work_item_id:, date: (date + 3.days).strftime('%Y-%m-%d'), percent: 40, definitive: false }) visit plannings_order_path(orders(:puzzletime)) + page.assert_selector('div.-definitive', count: 3) page.assert_selector('div.-provisional', count: 2) drag(row_pascal.all('.day')[0], row_pascal.all('.day')[1]) + page.assert_selector('#percent:focus') assert_equal '25', find('#percent').value assert_equal '', find('#percent')['placeholder'] @@ -65,6 +73,7 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest find('.planning-cancel').click drag(row_pascal.all('.day')[0], row_pascal.all('.day')[2]) + page.assert_selector('#percent:focus') assert_equal '25', find('#percent').value assert_equal '', find('#percent')['placeholder'] @@ -74,6 +83,7 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest find('.planning-cancel').click drag(row_pascal.all('.day')[2], row_pascal.all('.day')[3]) + page.assert_selector('#percent:not(:focus)') assert_equal '', find('#percent').value assert_equal '?', find('#percent')['placeholder'] @@ -83,6 +93,7 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest find('.planning-cancel').click drag(row_pascal.all('.day')[0], row_pascal.all('.day')[4]) + page.assert_selector('#percent:not(:focus)') assert_equal '', find('#percent').value assert_equal '?', find('#percent')['placeholder'] @@ -104,6 +115,7 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest page.assert_selector("#planned_order_#{orders(:puzzletime).id}", text: '6 von 100 Stunden verplant') drag(row_pascal.all('.day')[2], row_pascal.all('.day')[4]) + page.assert_selector('.-selected', count: 3) page.assert_selector('.planning-panel', visible: true) @@ -137,6 +149,7 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest row = find("#planning_row_employee_#{employees(:pascal).id}_work_item_#{work_items(:hitobito_demo_app).id}") drag(row.all('.day')[2], row.all('.day')[4]) + page.assert_selector('.planning-panel', visible: true) within '.planning-panel' do @@ -154,6 +167,7 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest test 'update planning entries' do drag(row_mark.all('.day')[0], row_pascal.all('.day')[0]) + page.assert_selector('.-selected', count: 2) page.assert_selector('.planning-panel', visible: true) @@ -172,6 +186,7 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest test 'create & update planning entries' do drag(row_mark.all('.day')[0], row_pascal.all('.day')[1]) + page.assert_selector('.-selected', count: 4) page.assert_selector('.planning-panel', visible: true) @@ -197,12 +212,14 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest page.driver.browser.manage.window.resize_to(1024, 756) drag(row_mark.all('.day')[0], row_mark.all('.day')[4]) + page.assert_selector('.-selected', count: 5) page.assert_selector('.planning-panel', visible: true) within '.planning-panel' do page.assert_no_selector('#repeat_until', visible: true) check 'repetition' + page.assert_selector('#repeat_until', visible: true) fill_in( @@ -219,9 +236,11 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest page.assert_selector('.planning-panel', visible: false) percents = ['50', '', '', '', '', '50', '', '', '', '', '50', '', '', '', '', ''] + assert_percents percents, row_mark drag(row_mark.all('.day')[0], row_mark.all('.day')[3]) + page.assert_selector('.-selected', count: 4) page.assert_selector('.planning-panel', visible: true) @@ -231,9 +250,10 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest page.assert_no_selector('#repeat_until', visible: true) check 'repetition' + page.assert_selector('#repeat_until', visible: true) - fill_in 'repeat_until', with: (today + 1.weeks).strftime('%Y %U') + fill_in 'repeat_until', with: (today + 1.week).strftime('%Y %U') # find('#percent').click # required to close calendar popover click_button 'OK' end @@ -244,6 +264,7 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest page.assert_selector('.planning-panel', visible: false) percents = ['30', '30', '30', '30', '', '30', '30', '30', '30', '', '50', '', '', '', '', ''] + assert_percents percents, row_mark end @@ -254,6 +275,7 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest find('.add').click selectize('add_employee_select_id', 'Dolores Pedro', no_click: true) + page.assert_selector('#planning_row_employee_2_work_item_4', text: 'Dolores Pedro') page.assert_selector('#planning_row_employee_2_work_item_4 .day', count: workdays_next_n_months(3)) @@ -261,15 +283,18 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest end test 'Select does not show already present employees' do - assert find('#planning_row_employee_7_work_item_4 .legend').text.include?('Waber Mark') + assert_includes find('#planning_row_employee_7_work_item_4 .legend').text, 'Waber Mark' find('.add').click + assert_not open_selectize('add_employee_select_id', no_click: true).text.include?('Waber Mark') end test 'Should not be able to move an empty selection' do drag(row_mark.all('.day')[5], row_pascal.all('.day')[9]) + page.assert_selector('.day.-selected', count: 10) drag(row_pascal.all('.day')[5], row_pascal.all('.day')[3]) + page.assert_selector('.day.-selected', count: 3) end @@ -283,13 +308,15 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest click_button 'OK' end - page.assert_selector('div.-definitive', count: 12) + within('.planning-calendar') do + assert_selector('div.-definitive', count: 12) + drag(row_mark.all('.day')[5], row_pascal.all('.day')[9]) - drag(row_mark.all('.day')[5], row_pascal.all('.day')[9]) - page.assert_selector('.day.-selected', count: 10) - drag(row_pascal.all('.day.-selected')[2], row_mark.all('.day')[0], row_mark.all('*')[0]) - row_mark.assert_selector('.day.-selected.-definitive:nth-child(2)') - page.assert_selector('.day.-definitive:not(.-selected)', count: 10, text: 100) + assert_selector('.day.-selected', count: 10) + drag(row_pascal.all('.day.-selected')[2], row_mark.all('.day')[0]) + + assert_selector('.day.-definitive', count: 10) + end end end @@ -305,8 +332,10 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest page.assert_selector('div.-definitive', count: 4) drag(row_pascal.all('.day')[1], row_pascal.all('.day')[2]) + page.assert_selector('.day.-selected', count: 2) drag(row_pascal.all('.day.-selected')[1], row_pascal.all('.day')[0]) + row_pascal.assert_selector('.day.-definitive:not(.-selected)', count: 2) row_pascal.assert_selector('.day.-definitive:not(.-selected)', count: 2, text: 100) end @@ -323,8 +352,10 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest page.assert_selector('div.-definitive', count: 4) drag(row_pascal.all('.day')[1], row_pascal.all('.day')[2]) + page.assert_selector('.day.-selected', count: 2) drag(row_pascal.all('.day.-selected')[1], row_pascal.all('.day')[3]) + row_pascal.assert_selector('.day.-definitive:not(.-selected)', count: 3) row_pascal.assert_selector('.day.-definitive:not(.-selected)', count: 1, text: 25) row_pascal.assert_selector('.day.-definitive:not(.-selected)', count: 2, text: 100) @@ -340,14 +371,15 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest end page.assert_selector('div.-definitive', count: 4) - drag(row_pascal.all('.day')[1], row_pascal.all('.day')[2]) + page.assert_selector('.day.-selected', count: 2) drag( row_pascal.all('.day')[2], row_pascal.all('.day')[8], row_pascal.all('.day')[2] ) + row_pascal.assert_selector('.day.-definitive', count: 3) row_pascal.assert_selector('.day.-definitive.-selected', count: 2) row_pascal.assert_selector('.day.-definitive:nth-child(2)', text: 25) @@ -357,27 +389,37 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest test 'delete plannings' do row_mark.all('.day')[1].click + page.assert_selector('.planning-panel', visible: true) page.assert_no_selector('.planning-delete', visible: true) find('.planning-cancel').click + page.assert_selector('.planning-panel', visible: false) drag(row_mark.all('.day')[0], row_pascal.all('.day')[0]) + page.assert_selector('.planning-panel', visible: true) page.assert_selector('.planning-delete', visible: true) find('.planning-cancel').click + page.assert_selector('.planning-panel', visible: false) drag(row_mark.all('.day')[0], row_pascal.all('.day')[1]) + page.assert_selector('.planning-panel', visible: true) page.assert_selector('.planning-delete', visible: true) - assert_difference('Planning.count', -2) do + # assert_difference('Planning.all.to_a.count', -2) do + accept_confirm('Bist du sicher, dass du die selektierte Planung löschen willst?') do find('.planning-delete').click - accept_confirmation('Bist du sicher, dass du die selektierte Planung löschen willst?') - page.assert_selector('.planning-panel', visible: false) - page.assert_selector('div.day.-definitive', count: 0) end + + page.assert_selector('.planning-panel', visible: false) + page.assert_selector('div.day.-definitive', count: 0) + # end + + # FIXME: Why the hell does this work, but Planning.count difference does not? + assert_equal 1, Planning.all.to_a.size end test 'switching period' do @@ -387,22 +429,26 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest select 'Nächste 6 Monate', from: 'period_shortcut' find('.navbar-brand').click # blur select + page.assert_selector('.planning-calendar-weeks', - text: "KW #{(Time.zone.today + 6.months - 1.weeks).cweek}") + text: "KW #{(Time.zone.today + 6.months - 1.week).cweek}") page.assert_selector('#start_date', visible: false) page.assert_selector('#end_date', visible: false) assert_equal '6M', find('#period_shortcut').value drag(row_mark.all('.day')[0], row_pascal.all('.day')[1]) + page.assert_selector('.-selected', count: 4) select 'benutzerdefiniert', from: 'period_shortcut' find('.navbar-brand').click # blur select + page.assert_selector('#start_date', visible: true) page.assert_selector('#end_date', visible: true) assert_equal '', find('#period_shortcut').value drag(row_mark.all('.day')[0], row_pascal.all('.day')[2]) + page.assert_selector('.-selected', count: 6) end @@ -410,24 +456,27 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest page.assert_selector('.planning-board-header', text: 'PITC-PT: PuzzleTime') select 'Nächste 6 Monate', from: 'period_shortcut' find('.navbar-brand').click # blur select + page.assert_selector('.planning-calendar-weeks', - text: "KW #{(Time.zone.today + 6.months - 1.weeks).cweek}") + text: "KW #{(Time.zone.today + 6.months - 1.week).cweek}") page.assert_selector('#start_date', visible: false) page.assert_selector('#end_date', visible: false) assert_equal '6M', find('#period_shortcut').value visit plannings_employee_path(employees(:mark)) + page.assert_selector('.planning-board-header', text: 'Waber Mark') page.assert_selector('.planning-calendar-weeks', - text: "KW #{(Time.zone.today + 6.months - 1.weeks).cweek}") + text: "KW #{(Time.zone.today + 6.months - 1.week).cweek}") page.assert_selector('#start_date', visible: false) page.assert_selector('#end_date', visible: false) assert_equal '6M', find('#period_shortcut').value visit plannings_company_path + page.assert_selector('h1', text: 'Planung aller Members') page.assert_selector('#plannings thead', - text: (Time.zone.today + 6.months - 1.weeks).cweek) + text: (Time.zone.today + 6.months - 1.week).cweek) page.assert_selector('#start_date', visible: false) page.assert_selector('#end_date', visible: false) assert_equal '6M', find('#period_shortcut').value @@ -435,13 +484,15 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest test 'add row still works after switching period' do find('.add').click + page.assert_selector('.selectize-dropdown') select 'Nächste 6 Monate', from: 'period_shortcut' find('.navbar-brand').click # blur select select 'Nächste 6 Monate', from: 'period_shortcut' # seems to only update value when selecting 2-times + page.assert_selector('.planning-calendar-weeks', - text: "KW #{(Time.zone.today + 6.months - 1.weeks).cweek}") + text: "KW #{(Time.zone.today + 6.months - 1.week).cweek}") page.assert_selector('#start_date,#end_date', visible: false) page.assert_no_selector('#add_employee_id') @@ -450,6 +501,7 @@ class PlanningsOrdersTest < ActionDispatch::IntegrationTest find('.add').click selectize('add_employee_select_id', 'Dolores Pedro', no_click: true) + page.assert_selector('#planning_row_employee_2_work_item_4', text: 'Dolores Pedro') page.assert_selector('#planning_row_employee_2_work_item_4 .day', count: workdays_next_n_months(6)) @@ -500,11 +552,13 @@ def assert_percents(percents, row) end def row_mark - find("#planning_row_employee_#{employees(:mark).id}_work_item_#{work_item_id}") + # TODO: without `sleep` I get "Node is either not visible or not an HTMLElement". Why?? + @row_mark ||= find("#planning_row_employee_#{employees(:mark).id}_work_item_#{work_item_id}").tap { sleep 0.1 } end def row_pascal - find("#planning_row_employee_#{employees(:pascal).id}_work_item_#{work_item_id}") + # TODO: without `sleep` I get "Node is either not visible or not an HTMLElement". Why?? + @row_pascal ||= find("#planning_row_employee_#{employees(:pascal).id}_work_item_#{work_item_id}").tap { sleep 0.1 } end def work_item_id @@ -514,13 +568,13 @@ def work_item_id def create_plannings(work_item_id) date = Time.zone.today.beginning_of_week.strftime('%Y-%m-%d') Planning.create!({ employee_id: employees(:pascal).id, - work_item_id: work_item_id, - date: date, + work_item_id:, + date:, percent: 25, definitive: true }) Planning.create!({ employee_id: employees(:mark).id, - work_item_id: work_item_id, - date: date, + work_item_id:, + date:, percent: 50, definitive: true }) end diff --git a/test/integration/show_order_services_test.rb b/test/integration/show_order_services_test.rb index c5f3b6f26..23702e238 100644 --- a/test/integration/show_order_services_test.rb +++ b/test/integration/show_order_services_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -12,6 +14,7 @@ class ShowOrderServices < ActionDispatch::IntegrationTest timeout_safe do create_ordertime_show_order_services_as employee_without_responsibilities click_worktime_row + assert has_no_text?('Zeit bearbeiten') assert_equal order_order_services_path(order_id: order), current_path end @@ -21,6 +24,7 @@ class ShowOrderServices < ActionDispatch::IntegrationTest timeout_safe do create_ordertime_show_order_services_as employee_responsible_for_order click_worktime_row + assert has_text?('Zeit bearbeiten') assert_equal edit_ordertime_path(id: ordertime.id), current_path end @@ -30,6 +34,7 @@ class ShowOrderServices < ActionDispatch::IntegrationTest timeout_safe do create_ordertime_show_order_services_as employee_responsible_for_different_order click_worktime_row + assert has_no_text?('Zeit bearbeiten') assert_equal order_order_services_path(order_id: order), current_path end @@ -39,6 +44,7 @@ class ShowOrderServices < ActionDispatch::IntegrationTest timeout_safe do create_ordertime_show_order_services_as manager_not_responsible_for_any_order click_worktime_row + assert has_text?('Zeit bearbeiten') assert_equal edit_ordertime_path(id: ordertime.id), current_path end @@ -52,6 +58,7 @@ class ShowOrderServices < ActionDispatch::IntegrationTest login_as user visit order_order_services_path(order_id: order) click_worktime_row + assert has_no_text?('Zeit bearbeiten') assert_equal order_order_services_path(order_id: order), current_path end @@ -60,31 +67,31 @@ class ShowOrderServices < ActionDispatch::IntegrationTest private def employee_without_responsibilities - Employee.where.not(management: true, id: responsible_ids).first.tap do |employee| - refute employee.management - refute employee.order_responsible? + Employee.where.not(management: true).where.not(id: responsible_ids).first.tap do |employee| + assert_not employee.management + assert_not_predicate employee, :order_responsible? end end def employee_responsible_for_order order.responsible.tap do |employee| - refute employee.management - assert employee.order_responsible? + assert_not employee.management + assert_predicate employee, :order_responsible? assert_equal employee, order.responsible end end def employee_responsible_for_different_order Employee.where(management: false, id: responsible_ids).where.not(id: order.responsible_id).first.tap do |employee| - refute employee.management - assert employee.order_responsible? - refute_equal employee, order.responsible + assert_not employee.management + assert_predicate employee, :order_responsible? + assert_not_equal employee, order.responsible end end def manager_not_responsible_for_any_order Employee.where(management: true).where.not(id: order.responsible_id).first.tap do |employee| - refute_equal employee, order.responsible + assert_not_equal employee, order.responsible assert employee.management end end @@ -95,7 +102,7 @@ def responsible_ids def create_ordertime(employee) @ordertime = Ordertime.create!( - employee: employee, + employee:, work_date: Time.zone.today, report_type: :absolute_day, hours: 0.5, diff --git a/test/integration/show_order_test.rb b/test/integration/show_order_test.rb index 5a9596f7f..d240ad9ee 100644 --- a/test/integration/show_order_test.rb +++ b/test/integration/show_order_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2021, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -6,7 +8,9 @@ require 'test_helper' class ShowOrder < ActionDispatch::IntegrationTest + setup :crm_start setup :login + teardown :crm_stop ADDITIONAL_CRM_LABEL_TEXT = 'Weitere Highrise IDs' @@ -29,6 +33,16 @@ class ShowOrder < ActionDispatch::IntegrationTest private + def crm_start + Settings.highrise.api_token = 'test' + Crm.init + end + + def crm_stop + Settings.highrise.api_token = nil + Crm.init + end + def login login_as(:mark) visit(order_path(order)) @@ -43,6 +57,6 @@ def additional_crm_links_label end def additional_crm_links - additional_crm_links_label.find(:xpath, "following-sibling::dd//ul", visible: false) + additional_crm_links_label.find(:xpath, 'following-sibling::dd//ul', visible: false) end end diff --git a/test/integration/workload_report_test.rb b/test/integration/workload_report_test.rb index 588d806cf..34218be5c 100644 --- a/test/integration/workload_report_test.rb +++ b/test/integration/workload_report_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -10,15 +12,16 @@ class WorkloadReportTest < ActionDispatch::IntegrationTest test 'member detail links are set-up on first page load' do find('a[data-toggle="employee-8-ordertimes"]').click + assert_selector 'tbody#employee-8-ordertimes' end test 'member detail links are set-up after changing filter' do - find('input#start_date').click - all('td[data-handler=selectDay]').last.click + set_period assert_no_selector 'tbody#employee-6-ordertimes' find('a[data-toggle="employee-6-ordertimes"]').click + assert_selector 'tbody#employee-6-ordertimes' end @@ -26,8 +29,7 @@ class WorkloadReportTest < ActionDispatch::IntegrationTest def login login_as(:mark) - visit reports_workload_path(start_date: '1.1.2006', - end_date: '31.12.2006', - department_id: departments(:devtwo).id) + set_period(start_date: '1.1.2006', end_date: '31.12.2006', back_url: '/') + visit reports_workload_path(department_id: departments(:devtwo).id) end end diff --git a/test/mailers/previews/employee_mailer_preview.rb b/test/mailers/previews/employee_mailer_preview.rb new file mode 100644 index 000000000..afbe648eb --- /dev/null +++ b/test/mailers/previews/employee_mailer_preview.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class EmployeeMailerPreview < ActionMailer::Preview + def worktime_deleted_mail + worktime_user = Employee.new(email: 'user@example.com', firstname: 'Peter', lastname: 'Puzzler') + worktime = Ordertime.new( + employee: worktime_user, + account: WorkItem.new(name: 'Lieblingsprojekt', path_shortnames: 'TOP-FAV'), + work_date: Time.zone.today, + hours: 4.33, + report_type: ReportType::HoursDayType::INSTANCE + ) + management_user = Employee.new(firstname: 'Mad', lastname: 'Manager') + EmployeeMailer.worktime_deleted_mail(worktime, management_user) + end + + def worktime_commit_reminder_mail + employee = Employee.new(email: 'user@example.com', firstname: 'Peter', lastname: 'Puzzler') + EmployeeMailer.worktime_commit_reminder_mail(employee) + end +end diff --git a/test/models/accounting_post_test.rb b/test/models/accounting_post_test.rb index 6017dcb52..2ef603868 100644 --- a/test/models/accounting_post_test.rb +++ b/test/models/accounting_post_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -29,8 +31,9 @@ class AccountingPostTest < ActiveSupport::TestCase post = accounting_posts(:webauftritt) order = post.order order.update!(status: order_statuses(:abgeschlossen)) + assert_equal post.work_item_id, order.work_item_id - assert_equal true, post.work_item.closed + assert post.work_item.closed fresh = nil assert_difference('WorkItem.count', 2) do fresh = AccountingPost.create!( @@ -42,19 +45,22 @@ class AccountingPostTest < ActiveSupport::TestCase end post.reload fresh.reload + assert_not_equal post.work_item_id, order.work_item_id assert_equal post.work_item_id, worktimes(:wt_pz_webauftritt).work_item_id - assert_equal true, fresh.work_item.leaf - assert_equal true, post.work_item.leaf - assert_equal true, fresh.work_item.closed - assert_equal true, post.work_item.closed + assert fresh.work_item.leaf + assert post.work_item.leaf + assert fresh.work_item.closed + assert post.work_item.closed end test 'creating new accounting post when order workitem is invalid sets flash message' do post = accounting_posts(:webauftritt) post.update_column(:offered_rate, nil) - refute post.reload.valid? + + assert_not_predicate post.reload, :valid? order = post.order + assert_equal post.work_item_id, order.work_item_id assert_no_difference('WorkItem.count') do fresh = AccountingPost.create( @@ -65,6 +71,7 @@ class AccountingPostTest < ActiveSupport::TestCase ) post.reload + assert_equal post.work_item_id, order.work_item_id assert_error_message fresh, :base, /Bestehende Buchungsposition ist ungültig/ end @@ -73,15 +80,18 @@ class AccountingPostTest < ActiveSupport::TestCase test 'opening post with closed order does not open work items' do closed = OrderStatus.where(closed: true).first post.order.update!(status: closed) - assert_equal true, post.work_item.reload.closed + + assert post.work_item.reload.closed post.update!(closed: true) post.update!(closed: false) - assert_equal true, post.work_item.reload.closed + + assert post.work_item.reload.closed opened = OrderStatus.where(closed: false).first post.order.update!(status: opened) - assert_equal false, post.work_item.reload.closed + + assert_not post.work_item.reload.closed end test 'opening order with closed post does not open work items' do @@ -91,13 +101,16 @@ class AccountingPostTest < ActiveSupport::TestCase opened = OrderStatus.where(closed: false).first post.order.update!(status: opened) - assert_equal true, post.work_item.reload.closed + + assert post.work_item.reload.closed post.update!(closed: false) - assert_equal false, post.work_item.reload.closed + + assert_not post.work_item.reload.closed post.order.update!(status: closed) - assert_equal true, post.work_item.reload.closed + + assert post.work_item.reload.closed end test 'destroying accounting post destroys work item' do @@ -134,12 +147,12 @@ class AccountingPostTest < ActiveSupport::TestCase order = Fabricate(:order) accounting_post = Fabricate(:accounting_post, work_item: order.work_item) 5.times do |i| - Fabricate(:planning, work_item: order.work_item, employee: employee) do + Fabricate(:planning, work_item: order.work_item, employee:) do date { Date.new(2017, 12, 18) + i } end end Fabricate.times(10, :ordertime, work_item: order.work_item, - employee: employee) + employee:) assert_equal order.work_item, accounting_post.work_item @@ -157,11 +170,13 @@ class AccountingPostTest < ActiveSupport::TestCase plannings = order.work_item.plannings.select do |p| p.work_item_id == order.work_item.id end + assert_equal 0, plannings.size worktimes = order.work_item.worktimes.select do |t| t.work_item_id == order.work_item.id end + assert_equal 0, worktimes.size assert_equal 5, accounting_post.plannings.count diff --git a/test/models/additional_crm_order_test.rb b/test/models/additional_crm_order_test.rb index 4020acf30..d3e8574a5 100644 --- a/test/models/additional_crm_order_test.rb +++ b/test/models/additional_crm_order_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # == Schema Information # # Table name: additional_crm_orders diff --git a/test/models/authentication_test.rb b/test/models/authentication_test.rb index f01e61d5b..6a0eb7772 100644 --- a/test/models/authentication_test.rb +++ b/test/models/authentication_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class AuthenticationTest < ActiveSupport::TestCase diff --git a/test/models/client_test.rb b/test/models/client_test.rb index 74859bdb3..4725a5c5b 100644 --- a/test/models/client_test.rb +++ b/test/models/client_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -50,22 +52,27 @@ class ClientTest < ActiveSupport::TestCase test 'e bill account key has 17 digits' do client = Client.new client.valid? - assert client.errors[:e_bill_account_key].blank? + + assert_predicate client.errors[:e_bill_account_key], :blank? client.e_bill_account_key = '41105678901234567' client.valid? - assert client.errors[:e_bill_account_key].blank? + + assert_predicate client.errors[:e_bill_account_key], :blank? client.e_bill_account_key = '411056789012345678' client.valid? - assert client.errors[:e_bill_account_key].present? + + assert_predicate client.errors[:e_bill_account_key], :present? client.e_bill_account_key = '4110567890123456' client.valid? - assert client.errors[:e_bill_account_key].present? + + assert_predicate client.errors[:e_bill_account_key], :present? client.e_bill_account_key = '12345678901234567' client.valid? - assert client.errors[:e_bill_account_key].present? + + assert_predicate client.errors[:e_bill_account_key], :present? end end diff --git a/test/models/contact_test.rb b/test/models/contact_test.rb new file mode 100644 index 000000000..812dd85e7 --- /dev/null +++ b/test/models/contact_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Copyright (c) 2006-2022, Puzzle ITC GmbH. This file is part of +# PuzzleTime and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/puzzle/puzzletime. + +require 'test_helper' + +class ContactTest < ActiveSupport::TestCase + def contact(email:) + Fabricate.build(:contact, email:, client: clients(:puzzle)) + end + + test 'email can be blank' do + assert_predicate contact(email: nil), :valid? + assert_predicate contact(email: ''), :valid? + end + + test 'email must be valid' do + assert_predicate contact(email: 'test.email+tag@example.com'), :valid? + assert_not contact(email: 'test').valid? + assert_not contact(email: 'example.com').valid? + assert_not contact(email: '@example.com').valid? + assert_not contact(email: 'test@email@example.com').valid? + assert_not contact(email: 'andré@example.com').valid? + end +end diff --git a/test/models/employee_test.rb b/test/models/employee_test.rb index 317d42e05..c0841397a 100644 --- a/test/models/employee_test.rb +++ b/test/models/employee_test.rb @@ -49,47 +49,51 @@ def setup def test_half_year_employment employee = Employee.find(1) period = year_period(employee) - assert_equal employee.statistics.employments_during(period).size, 1 + + assert_equal 1, employee.statistics.employments_during(period).size assert_in_delta 12.60, employee.statistics.remaining_vacations(period.end_date), 0.005 - assert_equal employee.statistics.used_vacations(period), 0 + assert_equal 0, employee.statistics.used_vacations(period) assert_in_delta 12.60, employee.statistics.total_vacations(period), 0.005 - assert_equal employee.statistics.overtime(period), - 127 * 8 + assert_equal -127 * 8, employee.statistics.overtime(period) end def test_various_employment employee = Employee.find(2) period = year_period(employee) employments = employee.statistics.employments_during(period) + assert_equal 3, employments.size assert_equal employments[0].start_date, Date.new(2005, 11, 1) assert_equal employments[0].end_date, Date.new(2006, 1, 31) - assert_equal employments[0].period.length, 92 + assert_equal 92, employments[0].period.length assert_in_delta 2.52, employments[0].vacations, 0.005 assert_in_delta 3.73, employments[1].vacations, 0.005 assert_in_delta 7.33, employments[2].vacations, 0.005 assert_in_delta 13.58, employee.statistics.remaining_vacations(period.end_date), 0.01 - assert_equal employee.statistics.used_vacations(period), 0 + assert_equal 0, employee.statistics.used_vacations(period) assert_in_delta 13.58, employee.statistics.total_vacations(period), 0.01 assert_in_delta 11.90, employee.statistics.total_vacations(Period.year_for(Date.new(2006))), 0.01 - assert_equal employee.statistics.overtime(period), - (64 * 0.4 * 8 + 162 * 0.2 * 8 + 73 * 8) + assert_equal employee.statistics.overtime(period), - ((64 * 0.4 * 8) + (162 * 0.2 * 8) + (73 * 8)) end def test_next_year_employment employee = Employee.find(3) period = year_period(employee) - assert_equal employee.statistics.employments_during(period).size, 0 - assert_equal employee.statistics.remaining_vacations(Date.new(2006, 12, 31)), 0 - assert_equal employee.statistics.used_vacations(period), 0 - assert_equal employee.statistics.total_vacations(period), 0 - assert_equal employee.statistics.overtime(period), 0 + + assert_equal 0, employee.statistics.employments_during(period).size + assert_equal 0, employee.statistics.remaining_vacations(Date.new(2006, 12, 31)) + assert_equal 0, employee.statistics.used_vacations(period) + assert_equal 0, employee.statistics.total_vacations(period) + assert_equal 0, employee.statistics.overtime(period) end def test_left_this_year_employment employee = Employee.find(4) period = year_period(employee) - assert_equal employee.statistics.employments_during(period).size, 1 + + assert_equal 1, employee.statistics.employments_during(period).size assert_in_delta 29.92, employee.statistics.remaining_vacations(period.end_date), 0.005 - assert_equal employee.statistics.used_vacations(period), 0 + assert_equal 0, employee.statistics.used_vacations(period) assert_in_delta 29.92, employee.statistics.total_vacations(period), 0.005 assert_in_delta((- 387 * 8 * 0.8), employee.statistics.overtime(period), 0.005) end @@ -97,20 +101,23 @@ def test_left_this_year_employment def test_long_time_employment employee = Employee.find(5) period = year_period(employee) - assert_equal employee.statistics.employments_during(period).size, 1 + + assert_equal 1, employee.statistics.employments_during(period).size assert_in_delta 382.5, employee.statistics.remaining_vacations(period.end_date), 0.005 - assert_equal employee.statistics.used_vacations(period), 0 + assert_equal 0, employee.statistics.used_vacations(period) assert_in_delta 382.5, employee.statistics.total_vacations(period), 0.005 - assert_equal employee.statistics.overtime(period), - 31_500 + assert_equal -31_500, employee.statistics.overtime(period) end def test_alltime_leaf_work_items e = employees(:pascal) + assert_equal work_items(:allgemein, :puzzletime, :webauftritt), e.alltime_leaf_work_items end def test_alltime_main_work_items e = employees(:pascal) + assert_equal work_items(:puzzle, :swisstopo), e.alltime_main_work_items end @@ -120,6 +127,7 @@ def test_alltime_main_work_items to = '11.12.2006' empls = Employee.with_worktimes_in_period(order, from, to) + assert 1, empls.size assert_includes empls, employees(:mark) end @@ -141,6 +149,20 @@ def test_alltime_main_work_items assert_arrays_match employees(:long_time_john, :various_pedro, :next_year_pablo), Employee.current end + test '#pending_worktimes_commit scope' do + Employee.update_all(committed_worktimes_at: nil) + + assert_predicate Employee.pending_worktimes_commit, :present? + + Employee.update_all(committed_worktimes_at: Date.today.beginning_of_month - 1.day) + + assert_predicate Employee.pending_worktimes_commit, :blank? + + Employee.update_all(committed_worktimes_at: Date.today.beginning_of_month - 2.days) + + assert_predicate Employee.pending_worktimes_commit, :present? + end + private def year_period(employee) diff --git a/test/models/employment_role_category_test.rb b/test/models/employment_role_category_test.rb index a8dbdf0e2..7224c2b50 100644 --- a/test/models/employment_role_category_test.rb +++ b/test/models/employment_role_category_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -15,6 +17,6 @@ class EmploymentRoleCategoryTest < ActiveSupport::TestCase test 'string representation matches name' do - assert_equal employment_role_categories(:management).to_s, 'Management' + assert_equal 'Management', employment_role_categories(:management).to_s end end diff --git a/test/models/employment_role_level_test.rb b/test/models/employment_role_level_test.rb index f4b8ce030..8c8d4259f 100644 --- a/test/models/employment_role_level_test.rb +++ b/test/models/employment_role_level_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -15,6 +17,6 @@ class EmploymentRoleLevelTest < ActiveSupport::TestCase test 'string representation matches name' do - assert_equal employment_role_levels(:senior).to_s, 'Senior' + assert_equal 'Senior', employment_role_levels(:senior).to_s end end diff --git a/test/models/employment_role_test.rb b/test/models/employment_role_test.rb index cf33ebd94..fa5ecdeda 100644 --- a/test/models/employment_role_test.rb +++ b/test/models/employment_role_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -18,6 +20,6 @@ class EmploymentRoleTest < ActiveSupport::TestCase test 'string representation matches name' do - assert_equal employment_roles(:software_engineer).to_s, 'Software Engineer' + assert_equal 'Software Engineer', employment_roles(:software_engineer).to_s end end diff --git a/test/models/employment_roles_employment_test.rb b/test/models/employment_roles_employment_test.rb index 7cf1d5a2f..64e34cfa8 100644 --- a/test/models/employment_roles_employment_test.rb +++ b/test/models/employment_roles_employment_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at diff --git a/test/models/employment_test.rb b/test/models/employment_test.rb index a62ef09e7..737359370 100644 --- a/test/models/employment_test.rb +++ b/test/models/employment_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -26,34 +28,38 @@ def setup def test_musttime half_year = Employment.find(1) - assert_equal half_year.period.length, 184 - assert_equal half_year.period.musttime, 127 * 8 - assert_equal half_year.percent_factor, 1 + + assert_equal 184, half_year.period.length + assert_equal 127 * 8, half_year.period.musttime + assert_equal 1, half_year.percent_factor assert_in_delta 12.602, half_year.vacations - assert_equal half_year.musttime, 127 * 8 + assert_equal 127 * 8, half_year.musttime various = Employment.find(2) - assert_equal various.period.length, 92 - assert_equal various.period.musttime, 64 * 8 - assert_equal various.percent_factor, 0.4 + + assert_equal 92, various.period.length + assert_equal 64 * 8, various.period.musttime + assert_in_delta(various.percent_factor, 0.4) assert_in_delta 2.52, various.vacations - assert_equal various.musttime, 64 * 8 * 0.4 + assert_equal 64 * 8 * 0.4, various.musttime open_end = Employment.find(4) open_end.end_date = Date.new(2006, 12, 31) - assert_equal open_end.period.length, 107 - assert_equal open_end.period.musttime, 73 * 8 - assert_equal open_end.percent_factor, 1 + + assert_equal 107, open_end.period.length + assert_equal 73 * 8, open_end.period.musttime + assert_equal 1, open_end.percent_factor assert_in_delta 7.328, open_end.vacations - assert_equal open_end.musttime, 73 * 8 + assert_equal 73 * 8, open_end.musttime with_vacations = Employment.find(3) - assert_equal with_vacations.period.length, 227 + + assert_equal 227, with_vacations.period.length assert_in_delta 3.731, with_vacations.vacations end def test_musttime_for_period - period = Period.new("1.9.2007", "30.9.2007") + period = Period.new('1.9.2007', '30.9.2007') assert_equal period.musttime, employments(:various_100).musttime(period) assert_equal period.musttime * 0.9, employments(:long_time).musttime(period) @@ -61,45 +67,53 @@ def test_musttime_for_period def test_periods_must_not_overlap employee = Employee.find(6) - _one = Fabricate(:employment, employee: employee, start_date: '1.1.2015', end_date: '31.5.2015', percent: 80) - _two = Fabricate(:employment, employee: employee, start_date: '1.6.2015', percent: 100) + _one = Fabricate(:employment, employee:, start_date: '1.1.2015', end_date: '31.5.2015', percent: 80) + _two = Fabricate(:employment, employee:, start_date: '1.6.2015', percent: 100) - open_end = Employment.new(employee: employee, start_date: '1.3.2015', percent: 50, + open_end = Employment.new(employee:, start_date: '1.3.2015', percent: 50, employment_roles_employments: [Fabricate.build(:employment_roles_employment)]) + assert_not_valid open_end, :base - closed = Employment.new(employee: employee, start_date: '1.3.2015', end_date: '31.3.2015', percent: 50, + closed = Employment.new(employee:, start_date: '1.3.2015', end_date: '31.3.2015', percent: 50, employment_roles_employments: [Fabricate.build(:employment_roles_employment)]) + assert_not_valid closed, :base - before = Employment.new(employee: employee, start_date: '1.1.2014', end_date: '31.12.2014', percent: 50, + before = Employment.new(employee:, start_date: '1.1.2014', end_date: '31.12.2014', percent: 50, employment_roles_employments: [Fabricate.build(:employment_roles_employment)]) + assert_valid before - after = Employment.new(employee: employee, start_date: '1.9.2015', percent: 50, - employment_roles_employments: [Fabricate.build(:employment_roles_employment)]) + after = Employment.new(employee:, start_date: '1.9.2015', percent: 50, + employment_roles_employments: [Fabricate.build(:employment_roles_employment)]) + assert_valid after end def test_before_create_updates_previous_end_date employee = Employee.find(6) - _one = Fabricate(:employment, employee: employee, start_date: '1.1.2015', end_date: '31.5.2015', percent: 80) - two = Fabricate(:employment, employee: employee, start_date: '1.6.2015', percent: 100) + _one = Fabricate(:employment, employee:, start_date: '1.1.2015', end_date: '31.5.2015', percent: 80) + two = Fabricate(:employment, employee:, start_date: '1.6.2015', percent: 100) + + after = Employment.create!(employee:, start_date: '1.9.2015', end_date: '31.12.2015', percent: 50, + employment_roles_employments: [Fabricate.build(:employment_roles_employment)]) - after = Employment.create!(employee: employee, start_date: '1.9.2015', end_date: '31.12.2015', percent: 50, - employment_roles_employments: [Fabricate.build(:employment_roles_employment)]) assert_equal Date.parse('31.8.2015'), two.reload.end_date - after2 = Employment.create!(employee: employee, start_date: '1.3.2016', percent: 50, - employment_roles_employments: [Fabricate.build(:employment_roles_employment)]) + Employment.create!(employee:, start_date: '1.3.2016', percent: 50, + employment_roles_employments: [Fabricate.build(:employment_roles_employment)]) + assert_equal Date.parse('31.12.2015'), after.reload.end_date - before = Employment.create!(employee: employee, start_date: '1.1.2014', percent: 50, + before = Employment.create!(employee:, start_date: '1.1.2014', percent: 50, employment_roles_employments: [Fabricate.build(:employment_roles_employment)]) + assert_equal Date.parse('31.12.2014'), before.end_date - before2 = Employment.create!(employee: employee, start_date: '1.1.2013', end_date: '1.6.2013', percent: 50, + before2 = Employment.create!(employee:, start_date: '1.1.2013', end_date: '1.6.2013', percent: 50, employment_roles_employments: [Fabricate.build(:employment_roles_employment)]) + assert_equal Date.parse('1.6.2013'), before2.end_date end @@ -113,6 +127,6 @@ def test_vactions end def new_employment(start_date, end_date, percent: 100) - Employment.new(start_date: start_date, end_date: end_date, percent: percent, vacation_days_per_year: 20) + Employment.new(start_date:, end_date:, percent:, vacation_days_per_year: 20) end end diff --git a/test/models/expense_test.rb b/test/models/expense_test.rb index 0ac31b4c2..d89f174e3 100644 --- a/test/models/expense_test.rb +++ b/test/models/expense_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # == Schema Information # # Table name: expenses @@ -34,6 +36,7 @@ class ExpenseTest < ActiveSupport::TestCase test '.by_month returns models grouped by month' do hash = Expense.by_month(Expense.list, 2019) + assert_equal ['Februar 2019', 'Januar 2019'], hash.keys end @@ -42,7 +45,7 @@ class ExpenseTest < ActiveSupport::TestCase end test "pascal can not manage mark's invoices" do - refute can?(:manage, pascal, mark.expenses.build) + assert_not can?(:manage, pascal, mark.expenses.build) end test "mark can manage pascal's invoices" do @@ -53,6 +56,7 @@ class ExpenseTest < ActiveSupport::TestCase assert_equal [Date.new(2019, 2, 1), Date.new(2019, 3, 1)], Expense.reimbursement_months expenses(:approved).update!(reimbursement_date: Date.new(2019, 2, 1)) + assert_equal [Date.new(2019, 2, 1)], Expense.reimbursement_months end @@ -60,26 +64,29 @@ class ExpenseTest < ActiveSupport::TestCase assert_equal [Date.new(2019, 1, 1)], Expense.payment_years(pascal) expenses(:approved).update!(review_attrs.merge(payment_date: Date.new(2019, 2, 28))) + assert_equal [Date.new(2019, 1, 1)], Expense.payment_years(pascal) expenses(:pending).update!(payment_date: Date.new(2020, 2, 28)) + assert_equal [Date.new(2019, 1, 1), Date.new(2020, 1, 1)], Expense.payment_years(pascal) end test 'can only approve expense when reimbursement_date and reviewer is set' do obj = expenses(:pending) - refute obj.update(status: :approved) + + assert_not obj.update(status: :approved) assert_equal ['Auszahlungsmonat muss ausgefüllt werden', 'Reviewer muss ausgefüllt werden', 'Visiert am muss ausgefüllt werden'], obj.errors.full_messages assert obj.update(status: :approved, reviewer: mark, - reviewed_at: Date.today, - reimbursement_date: Date.today,) + reviewed_at: Time.zone.today, + reimbursement_date: Time.zone.today) end def review_attrs - { reviewer: mark, reviewed_at: Date.today } + { reviewer: mark, reviewed_at: Time.zone.today } end def mark diff --git a/test/models/invoice_test.rb b/test/models/invoice_test.rb index aea6d631b..21ab6cb83 100644 --- a/test/models/invoice_test.rb +++ b/test/models/invoice_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -28,7 +30,11 @@ class InvoiceTest < ActiveSupport::TestCase setup do - @worktime_lw2 = worktimes(:wt_lw_webauftritt).dup.tap { |w| w.work_date += 1.day; w.hours = 10; w.save! } + @worktime_lw2 = worktimes(:wt_lw_webauftritt).dup.tap do |w| + w.work_date += 1.day + w.hours = 10 + w.save! + end Invoicing.instance = nil invoice.employees = [employees(:pascal), employees(:mark), employees(:lucien)] invoice.work_items << work_items(:webauftritt) @@ -44,84 +50,105 @@ class InvoiceTest < ActiveSupport::TestCase test 'period must be positive' do invoice.period_to = invoice.period_from + assert_valid invoice invoice.period_to = invoice.period_to - 1.day + assert_not_valid invoice, :period_to end test 'billing address may belong to any client' do invoice.billing_address = billing_addresses(:puzzle) + assert_valid invoice end test 'validates period_from and period to' do invoice.period_from = invoice.period_to = nil - refute invoice.valid? + + assert_not_predicate invoice, :valid? assert_includes invoice.errors.messages[:period_from], 'muss ausgefüllt werden' assert_includes invoice.errors.messages[:period_to], 'muss ausgefüllt werden' invoice.period_from = invoice.period_to = '01.20.2000' - refute invoice.valid? + + assert_not_predicate invoice, :valid? assert_includes invoice.errors.messages[:period_from], 'muss ausgefüllt werden' assert_includes invoice.errors.messages[:period_to], 'muss ausgefüllt werden' invoice.period_from = '02.12.2000' invoice.period_to = '01.12.2000' - refute invoice.valid? + + assert_not_predicate invoice, :valid? assert_includes invoice.errors.messages[:period_to], 'muss nach von sein.' invoice.period_to = invoice.period_from - assert invoice.valid? + + assert_predicate invoice, :valid? end test 'validates order ist not closed' do orders(:webauftritt).update!(status: order_statuses(:abgeschlossen)) - refute invoice.valid? + + assert_not_predicate invoice, :valid? end test 'invoices with closed order cannot be destroyed' do orders(:webauftritt).update!(status: order_statuses(:abgeschlossen)) - refute invoice.destroy + + assert_not invoice.destroy end test 'generates invoice number' do - second_invoice = invoice.dup.tap { |i| i.reference = nil; i.save! } - assert_equal %w(STOP WEB D1 0002).join, second_invoice.reference + second_invoice = invoice.dup.tap do |i| + i.reference = nil + i.save! + end + + assert_equal %w[STOP WEB D1 0002].join, second_invoice.reference end test 'includes category shortname in invoice number' do - second_invoice = invoice_with_category.dup.tap { |i| i.reference = nil; i.save! } - assert_equal %w(PITC HIT DEM D2 0002).join, second_invoice.reference + second_invoice = invoice_with_category.dup.tap do |i| + i.reference = nil + i.save! + end + + assert_equal %w[PITC HIT DEM D2 0002].join, second_invoice.reference end test 'updates totals when validating' do invoice.total_amount = invoice.total_hours = 0 invoice.valid? + assert_equal 28, invoice.total_hours.to_f assert_equal 3920, invoice.total_amount.to_f end test 'rounds total_amount to nearest 5 cents' do - accounting_posts(:webauftritt).update_attribute(:offered_rate, 1.01) + accounting_posts(:webauftritt).update_attribute(:offered_rate, 1.01) invoice.valid? - assert_equal 28.28, invoice.send(:positions).collect(&:total_amount).sum.to_f - assert_equal 28.30, invoice.total_amount.to_f + assert_in_delta(28.28, invoice.send(:positions).sum(&:total_amount).to_f) + assert_in_delta(28.30, invoice.total_amount.to_f) end test 'manual_invoice? is true only when grouping == "manual"' do invoice.grouping = 'manual' - assert invoice.manual_invoice? + + assert_predicate invoice, :manual_invoice? (Invoice.groupings.keys - ['manual']).each do |grouping| invoice.grouping = grouping - refute invoice.manual_invoice? + + assert_not_predicate invoice, :manual_invoice? end end test 'grouping= accepts only valid values' do - %w(accounting_posts employees manual).each do |grouping| + %w[accounting_posts employees manual].each do |grouping| invoice.grouping = grouping + assert_equal grouping, invoice.grouping end @@ -132,18 +159,21 @@ class InvoiceTest < ActiveSupport::TestCase test 'calculated_total_amount when grouping = manual' do invoice.manual! - assert_equal 1.0, invoice.calculated_total_amount.to_f + + assert_in_delta(1.0, invoice.calculated_total_amount.to_f) end - %w(employees accounting_posts).each do |grouping| + %w[employees accounting_posts].each do |grouping| test "calculated_total_amount when grouping = #{grouping}" do invoice.grouping = grouping + assert_equal 3920, invoice.calculated_total_amount.to_f end end test 'worktimes' do worktimes = invoice.send(:worktimes) + assert_equal 3, worktimes.size assert_equal [worktimes(:wt_mw_webauftritt), worktimes(:wt_lw_webauftritt), @worktime_lw2].sort, worktimes.sort end @@ -151,6 +181,7 @@ class InvoiceTest < ActiveSupport::TestCase test 'worktimes does not include worktimes belonging to other invoice' do @worktime_lw2.update_attribute(:invoice_id, invoice.id + 1) worktimes = invoice.send(:worktimes) + assert_equal 2, worktimes.size assert_equal [worktimes(:wt_mw_webauftritt), worktimes(:wt_lw_webauftritt)].sort, worktimes.sort end @@ -158,6 +189,7 @@ class InvoiceTest < ActiveSupport::TestCase test 'worktimes included only for selected invoice employees' do invoice.employees = [employees(:mark)] worktimes = invoice.send(:worktimes) + assert_equal 1, worktimes.size assert_equal [worktimes(:wt_mw_webauftritt)], worktimes end @@ -166,6 +198,7 @@ class InvoiceTest < ActiveSupport::TestCase other_work_item = Fabricate(:work_item, parent_id: work_items(:webauftritt).parent_id) worktimes(:wt_lw_webauftritt).update_column(:work_item_id, other_work_item.id) worktimes = invoice.send(:worktimes) + assert_equal 2, worktimes.size assert_equal [worktimes(:wt_mw_webauftritt), @worktime_lw2].sort, worktimes.sort end @@ -173,6 +206,7 @@ class InvoiceTest < ActiveSupport::TestCase test 'build_positions when grouping is manual' do invoice.manual! positions = invoice.send(:build_positions) + assert 1, positions.size assert_equal 'Manuell', positions.first.name assert_equal 1, positions.first.total_hours @@ -182,8 +216,10 @@ class InvoiceTest < ActiveSupport::TestCase test 'build_positions when grouping is employees' do invoice.employees! positions = invoice.send(:build_positions).sort_by(&:name) + assert_equal 2, positions.size expected_position_names = [employees(:mark), employees(:lucien)].map { |e| "Webauftritt - #{e}" } + assert_equal expected_position_names, positions.map(&:name) assert_equal 7, positions.first.total_hours @@ -196,6 +232,7 @@ class InvoiceTest < ActiveSupport::TestCase test 'build_positions when grouping is work_items' do invoice.accounting_posts! positions = invoice.send(:build_positions) + assert_equal 1, positions.size assert_equal accounting_posts(:webauftritt).name, positions.first.name @@ -207,6 +244,7 @@ class InvoiceTest < ActiveSupport::TestCase Invoicing.instance = mock Invoicing.instance.stubs(:save_invoice).returns('abc123') invoice.save! + assert_equal 'abc123', invoice.invoicing_key end @@ -214,9 +252,10 @@ class InvoiceTest < ActiveSupport::TestCase Invoicing.instance = mock Invoicing.instance.stubs(:save_invoice).raises(Invoicing::Error.new('some invoicing error')) invoice.save - assert invoice.changed? + + assert_predicate invoice, :changed? assert_nil invoice.invoicing_key - assert_equal invoice.errors.messages, base: ['Fehler im Invoicing Service: some invoicing error'] + assert_equal({ base: ['Fehler im Invoicing Service: some invoicing error'] }, invoice.errors.messages) end test 'save assigns worktimes to invoice when successful' do @@ -225,6 +264,7 @@ class InvoiceTest < ActiveSupport::TestCase # committed worktimes may still be assigned to an invoice employees(:lucien).update!(committed_worktimes_at: '31.1.2015') invoice.save! + assert_equal invoice, worktimes(:wt_mw_webauftritt).reload.invoice assert_equal invoice, worktimes(:wt_lw_webauftritt).reload.invoice assert_equal invoice, @worktime_lw2.reload.invoice @@ -235,6 +275,7 @@ class InvoiceTest < ActiveSupport::TestCase Invoicing.instance.stubs(:save_invoice).returns('abc123') accounting_posts(:webauftritt).update!(closed: true) invoice.save! + assert_equal invoice, worktimes(:wt_mw_webauftritt).reload.invoice assert_equal invoice, worktimes(:wt_lw_webauftritt).reload.invoice assert_equal invoice, @worktime_lw2.reload.invoice @@ -244,28 +285,32 @@ class InvoiceTest < ActiveSupport::TestCase Invoicing.instance = mock Invoicing.instance.stubs(:save_invoice).raises(Invoicing::Error.new('some invoicing error')) invoice.save - refute_equal invoice, worktimes(:wt_mw_webauftritt).reload.invoice - refute_equal invoice, worktimes(:wt_lw_webauftritt).reload.invoice - refute_equal invoice, @worktime_lw2.reload.invoice + + assert_not_equal invoice, worktimes(:wt_mw_webauftritt).reload.invoice + assert_not_equal invoice, worktimes(:wt_lw_webauftritt).reload.invoice + assert_not_equal invoice, @worktime_lw2.reload.invoice end test 'save clears worktimes when setting grouping to manual' do invoice.save - refute_empty Worktime.where(invoice_id: invoice.id) + + assert_not_empty Worktime.where(invoice_id: invoice.id) invoice.grouping = 'manual' invoice.save + assert_empty Worktime.where(invoice_id: invoice.id) end test 'save removes worktimes of employees not assigned to invoice' do invoice.save # assert precondition - refute_empty Worktime.where(employee: employees(:mark), invoice: invoice) + assert_not_empty Worktime.where(employee: employees(:mark), invoice:) # remove employee, assert worktimes unassigned invoice.employees -= [employees(:mark)] invoice.save - assert_empty Worktime.where(employee: employees(:mark), invoice: invoice) + + assert_empty Worktime.where(employee: employees(:mark), invoice:) end test 'create stores last billing address on order' do @@ -274,6 +319,7 @@ class InvoiceTest < ActiveSupport::TestCase invoice.id = nil invoice.billing_address = billing_addresses(:swisstopo_2) invoice.save! + assert_equal billing_addresses(:swisstopo_2).id, orders(:webauftritt).reload.billing_address_id end @@ -282,6 +328,7 @@ class InvoiceTest < ActiveSupport::TestCase invoice = invoices(:webauftritt_may) invoice.billing_address = billing_addresses(:swisstopo_2) invoice.save! + assert_equal billing_addresses(:swisstopo_2).id, orders(:webauftritt).reload.billing_address_id end @@ -294,7 +341,7 @@ class InvoiceTest < ActiveSupport::TestCase test 'delete adds error message if invoicing error' do Invoicing.instance = mock Invoicing.instance.expects(:delete_invoice).raises(Invoicing::Error.new('some invoicing error')) - assert_no_difference('Invoice.count') { assert !invoice.destroy } + assert_no_difference('Invoice.count') { assert_not invoice.destroy } assert_equal ['Fehler im Invoicing Service: some invoicing error'], invoice.errors[:base] end @@ -307,9 +354,9 @@ def invoice def invoice_with_category @invoice_with_category ||= begin order = orders(:hitobito_demo) - Fabricate(:contract, order: order) unless order.contract + Fabricate(:contract, order:) unless order.contract Fabricate(:invoice, - order: order, + order:, work_items: [work_items(:hitobito_demo_app)], employees: [employees(:pascal)], period_to: Time.zone.today.at_end_of_month) @@ -321,8 +368,8 @@ class InvoiceTransactionTest < ActiveSupport::TestCase self.use_transactional_tests = false test 'generates different parallel invoice numbers' do - ActiveRecord::Base.clear_active_connections! - 10.times.collect do + ActiveRecord::Base.connection_handler.clear_active_connections! + Array.new(10) do Thread.new do ActiveRecord::Base.connection_pool.with_connection do invoices(:webauftritt_may).dup.save! diff --git a/test/models/order_contact_test.rb b/test/models/order_contact_test.rb index af2f10f16..af7e840b2 100644 --- a/test/models/order_contact_test.rb +++ b/test/models/order_contact_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -20,9 +22,13 @@ class OrderContactTest < ActiveSupport::TestCase test 'list scope is ordered by contact' do order = Fabricate(:order) - m = OrderContact.create!(order: order, contact: Fabricate(:contact, lastname: 'Miller', client: order.client)) - a = OrderContact.create!(order: order, contact: Fabricate(:contact, lastname: 'Aber', client: order.client)) - assert_equal [a, m], order.order_contacts.list + m = OrderContact.create!(order:, contact: Fabricate(:contact, lastname: 'Miller', client: order.client)) + a = OrderContact.create!(order:, contact: Fabricate(:contact, lastname: 'Aber', client: order.client)) + + expected = [a, m].map(&:attributes) + received = order.order_contacts.list.map(&:attributes) + + assert_equal expected, received end test 'crm ids are replaced' do diff --git a/test/models/order_status_test.rb b/test/models/order_status_test.rb index 13c59f326..68c36ff21 100644 --- a/test/models/order_status_test.rb +++ b/test/models/order_status_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -21,6 +23,7 @@ class OrderStatusTest < ActiveSupport::TestCase test 'closed is propagated to all order work items' do status = order_statuses(:bearbeitung) status.update!(closed: true) + assert work_items(:hitobito_demo_app).closed assert work_items(:hitobito_demo_site).closed assert work_items(:puzzletime).closed @@ -29,7 +32,8 @@ class OrderStatusTest < ActiveSupport::TestCase test 'opened is propagated to all order work items' do status = order_statuses(:abgeschlossen) status.update!(closed: false) - assert !work_items(:allgemein).closed + + assert_not work_items(:allgemein).closed end test 'defaults scope lists only default statuses' do diff --git a/test/models/order_test.rb b/test/models/order_test.rb index cd53c5d01..6f47176ea 100644 --- a/test/models/order_test.rb +++ b/test/models/order_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -30,17 +32,20 @@ class OrderTest < ActiveSupport::TestCase test 'order without client is not valid' do order = Fabricate(:order) - item = order.build_work_item(name: 'New Order', shortname: 'NEOR') - assert !order.valid? + order.build_work_item(name: 'New Order', shortname: 'NEOR') + + assert_not order.valid? end test 'order with client is valid' do order = Fabricate.build(:order) _item = order.build_work_item(name: 'New Order', shortname: 'NEOR', parent_id: work_items(:puzzle).id) - assert order.valid?, order.errors.full_messages.join(', ') + + assert_predicate order, :valid?, order.errors.full_messages.join(', ') assert_equal clients(:puzzle), order.client order.save! order = Order.find(order.id) + assert_equal clients(:puzzle), order.client end @@ -48,37 +53,44 @@ class OrderTest < ActiveSupport::TestCase order = Fabricate.build(:order) cat = Fabricate(:work_item, name: 'New Cat', shortname: 'NECA', parent_id: work_items(:puzzle).id) _item = order.build_work_item(name: 'New Order', shortname: 'NEOR', parent_id: cat.id) - assert order.valid?, order.errors.full_messages.join(', ') + + assert_predicate order, :valid?, order.errors.full_messages.join(', ') assert_equal clients(:puzzle), order.client order.save! order = Order.find(order.id) + assert_equal clients(:puzzle), order.client end test 'order is created with status' do order = Fabricate(:order) + assert_equal OrderStatus.defaults.first, order.status end test 'created order comes with order targets' do order = Fabricate(:order) scopes = TargetScope.all - assert scopes.size > 0 - assert_equal scopes.to_set, order.targets.collect(&:target_scope).to_set + + assert_operator scopes.size, :>, 0 + assert_equal scopes.to_set, order.targets.to_set(&:target_scope) end test 'accounting posts on lower level are accessible through work items' do order = orders(:hitobito_demo) + assert_equal accounting_posts(:hitobito_demo_app, :hitobito_demo_site).to_set, order.accounting_posts.to_set end test 'accounting post on same level is accessible through work items' do order = orders(:puzzletime) + assert_equal [accounting_posts(:puzzletime)], order.accounting_posts end test 'client is accessible through work items' do order = orders(:hitobito_demo) + assert_equal clients(:puzzle), order.client end @@ -107,20 +119,22 @@ class OrderTest < ActiveSupport::TestCase order.status = order_statuses(:bearbeitung) order.save! - assert !work_items(:hitobito_demo).closed + assert_not work_items(:hitobito_demo).closed assert work_items(:hitobito_demo_site).closed - assert !work_items(:hitobito_demo_app).closed + assert_not work_items(:hitobito_demo_app).closed end test 'default_billing_address_id is nil when last_billing_address is blank' do order = Fabricate(:order) order.billing_address = nil + assert_nil order.default_billing_address_id end test 'default_billing_address_id from client when last_billing_address is blank' do order = Fabricate(:order, work_item: Fabricate(:work_item, parent: clients(:swisstopo).work_item)) order.billing_address = nil + assert_equal(billing_addresses(:swisstopo).id, order.default_billing_address_id) end @@ -128,6 +142,7 @@ class OrderTest < ActiveSupport::TestCase order = Fabricate(:order, work_item: Fabricate(:work_item, parent: clients(:swisstopo).work_item)) [billing_addresses(:swisstopo), billing_addresses(:swisstopo_2)].each do |address| order.billing_address = address + assert_equal(address.id, order.default_billing_address_id) end end diff --git a/test/models/order_uncertainty_test.rb b/test/models/order_uncertainty_test.rb index 4804e3180..975e95510 100644 --- a/test/models/order_uncertainty_test.rb +++ b/test/models/order_uncertainty_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -23,9 +25,11 @@ class OrderRiskTest < ActiveSupport::TestCase test '#risk_value is probability times impact' do r1 = OrderRisk.new(name: 'Earthquake', probability: :low, impact: :medium) + assert_equal 6, r1.risk_value r2 = OrderRisk.new(name: 'Atomic desaster', probability: :improbable, impact: :high) + assert_equal 4, r2.risk_value end @@ -60,6 +64,7 @@ class OrderRiskTest < ActiveSupport::TestCase impact: :high) order.reload + assert_equal 4, order.major_risk_value assert_nil order.major_chance_value @@ -68,6 +73,7 @@ class OrderRiskTest < ActiveSupport::TestCase impact: :medium) order.reload + assert_equal 6, order.major_risk_value assert_nil order.major_chance_value @@ -76,6 +82,7 @@ class OrderRiskTest < ActiveSupport::TestCase impact: :none) order.reload + assert_equal 6, order.major_risk_value assert_nil order.major_chance_value @@ -84,18 +91,21 @@ class OrderRiskTest < ActiveSupport::TestCase impact: :high) order.reload + assert_equal 6, order.major_risk_value assert_equal 8, order.major_chance_value r.update!(probability: :medium) order.reload + assert_equal 6, order.major_risk_value assert_equal 12, order.major_chance_value order.order_risks.destroy_all order.reload + assert_nil order.major_risk_value assert_equal 12, order.major_chance_value end diff --git a/test/models/ordertime_test.rb b/test/models/ordertime_test.rb index 7ff7e9f2f..b6a19b89e 100644 --- a/test/models/ordertime_test.rb +++ b/test/models/ordertime_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -35,6 +37,7 @@ class OrdertimeTest < ActiveSupport::TestCase work_items(:webauftritt).update!(closed: true) t.reload t.work_date = '2015-08-31' + assert_not_valid t, :base end @@ -47,6 +50,7 @@ class OrdertimeTest < ActiveSupport::TestCase work_items(:webauftritt).update!(closed: true) t.reload t.work_item = work_items(:hitobito_demo_app) + assert_not_valid t, :base end @@ -58,30 +62,33 @@ class OrdertimeTest < ActiveSupport::TestCase report_type: 'absolute_day') work_items(:hitobito_demo_app).update!(closed: true) t.work_item = work_items(:hitobito_demo_app) + assert_not_valid t, :base end test 'worktime times must be 00:00-23:59' do t = Ordertime.create(employee: employees(:pascal), - work_date: '2015-10-10', - from_start_time: '00:00', - to_end_time: '24:00', - work_item: work_items(:webauftritt), - report_type: 'start_stop_day') + work_date: '2015-10-10', + from_start_time: '00:00', + to_end_time: '24:00', + work_item: work_items(:webauftritt), + report_type: 'start_stop_day') - refute t.valid?, t.errors.details[:to_end_time].join(', ') + assert_not_predicate t, :valid?, t.errors.details[:to_end_time].join(', ') end test '#invoice_sent_or_paid?' do t = Ordertime.new - assert !t.invoice_sent_or_paid? + + assert_not t.invoice_sent_or_paid? [['draft', false], ['sent', true], ['paid', true], ['partially_paid', true], ['deleted', false]].each do |status, result| - t.invoice = Invoice.new(status: status) + t.invoice = Invoice.new(status:) + assert_equal result, t.invoice_sent_or_paid?, "Status '#{status}', result should be #{result}" end end diff --git a/test/models/planning_test.rb b/test/models/planning_test.rb index 83deac92b..ac6c7e5db 100644 --- a/test/models/planning_test.rb +++ b/test/models/planning_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -20,21 +22,21 @@ class PlanningTest < ActiveSupport::TestCase test 'is invalid for weekends' do (Date.new(2000, 1, 1)..Date.new(2000, 1, 2)).each do |date| - assert Planning.new(employee_id: employee_id, - work_item_id: work_item_id, - date: date, - percent: 50, - definitive: true).invalid? + assert_predicate Planning.new(employee_id:, + work_item_id:, + date:, + percent: 50, + definitive: true), :invalid? end end test 'is valid for weekdays' do (Date.new(2000, 1, 3)..Date.new(2000, 1, 7)).each do |date| - assert Planning.new(employee_id: employee_id, - work_item_id: work_item_id, - date: date, - percent: 50, - definitive: true).valid? + assert_predicate Planning.new(employee_id:, + work_item_id:, + date:, + percent: 50, + definitive: true), :valid? end end diff --git a/test/models/user_notification_test.rb b/test/models/user_notification_test.rb index 64fa66681..8f9c2d7df 100644 --- a/test/models/user_notification_test.rb +++ b/test/models/user_notification_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -24,54 +26,9 @@ class UserNotificationTest < ActiveSupport::TestCase Holiday.create!(holiday_date: monday + 4.days, musthours_day: 0) messages = UserNotification.list_during.collect(&:message) + assert_includes messages, 'bar' assert_not_includes messages, 'foo' assert_includes messages, 'baz' end - - test 'worktimes commit notification not shown before end of month' do - ActiveSupport::TimeZone.any_instance.stubs(today: Date.new(2015, 9, 23)) - e = employees(:pascal) - assert !UserNotification.show_worktimes_commit_notification?(e) - end - - test 'worktimes commit notification shown at end of month' do - ActiveSupport::TimeZone.any_instance.stubs(today: Date.new(2015, 9, 24)) - e = employees(:pascal) - assert UserNotification.show_worktimes_commit_notification?(e) - end - - test 'worktimes commit notification shown at beginning of month' do - ActiveSupport::TimeZone.any_instance.stubs(today: Date.new(2015, 9, 3)) - e = employees(:pascal) - e.update!(committed_worktimes_at: '2015-7-31') - assert UserNotification.show_worktimes_commit_notification?(e) - end - - test 'worktimes commit notification not shown after beginning of month' do - ActiveSupport::TimeZone.any_instance.stubs(today: Date.new(2015, 9, 4)) - e = employees(:pascal) - assert !UserNotification.show_worktimes_commit_notification?(e) - end - - test 'worktimes commit notification not shown if committed this period end of month' do - ActiveSupport::TimeZone.any_instance.stubs(today: Date.new(2015, 9, 29)) - e = employees(:pascal) - e.update!(committed_worktimes_at: '2015-9-30') - assert !UserNotification.show_worktimes_commit_notification?(e) - end - - test 'worktimes commit notification not shown if committed this period beginning of month' do - ActiveSupport::TimeZone.any_instance.stubs(today: Date.new(2015, 10, 2)) - e = employees(:pascal) - e.update!(committed_worktimes_at: '2015-9-30') - assert !UserNotification.show_worktimes_commit_notification?(e) - end - - test 'worktimes commit notification not shown if committed next period beginning of month' do - ActiveSupport::TimeZone.any_instance.stubs(today: Date.new(2015, 10, 2)) - e = employees(:pascal) - e.update!(committed_worktimes_at: '2015-10-31') - assert !UserNotification.show_worktimes_commit_notification?(e) - end end diff --git a/test/models/util/employee_statistics_test.rb b/test/models/util/employee_statistics_test.rb index 26066c33b..fd000e5f7 100644 --- a/test/models/util/employee_statistics_test.rb +++ b/test/models/util/employee_statistics_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -11,6 +13,7 @@ class EmployeeStatisticsTest < ActiveSupport::TestCase test '#employments_during with start and end date set' do period = Period.new(Date.new(2000, 1, 1), Date.new(2000, 1, 23)) employments = statistics.employments_during(period) + assert_equal 1, employments.count assert_equal 10, employments.first.percent end @@ -18,6 +21,7 @@ class EmployeeStatisticsTest < ActiveSupport::TestCase test '#employments_during with only start date set' do period = Period.new(Date.new(2000, 1, 1), nil) employments = statistics.employments_during(period) + assert_equal 3, employments.count assert_equal 10, employments.first.percent assert_equal 20, employments.second.percent @@ -27,6 +31,7 @@ class EmployeeStatisticsTest < ActiveSupport::TestCase test '#employments_during with only end date set' do period = Period.new(nil, Date.new(2000, 1, 23)) employments = statistics.employments_during(period) + assert_equal 2, employments.count assert_equal 30, employments.first.percent assert_equal 10, employments.second.percent @@ -42,6 +47,27 @@ class EmployeeStatisticsTest < ActiveSupport::TestCase end end + test 'musttime calculates correctly' do + period = Period.new('01.11.2006', '30.11.2006') + + employee = Fabricate(:employee) + employment = Fabricate(:employment, employee:, percent: 100, start_date: '01.01.2006', end_date: '31.12.2006') + + assert_in_delta(176.0, employee.statistics.musttime(period)) + + employment.update(end_date: '15.11.2006') + + assert_in_delta(88.0, employee.statistics.musttime(period)) + + employment2 = Fabricate(:employment, employee:, percent: 100, start_date: '16.11.2006', end_date: '31.12.2006') + + assert_in_delta(176.0, employee.statistics.musttime(period)) + + employment2.update(percent: 50) + + assert_in_delta(132.0, employee.statistics.musttime(period)) + end + test 'remaining worktime is affected by' do period = Period.new('01.12.2006', '11.12.2006') method = 'statistics.pending_worktime(period).to_f' @@ -55,6 +81,76 @@ class EmployeeStatisticsTest < ActiveSupport::TestCase end # rubocop:enable Lint/UselessAssignment + test '#average_percents calculates correctly' do + period = Period.new('01.11.2006', '30.11.2006') + + employee = Fabricate(:employee) + employment = Fabricate(:employment, employee:, percent: 100, start_date: '01.01.2006', end_date: '31.12.2006') + + assert_in_delta(100.0, employee.statistics.average_percents(period)) + + employment.update(end_date: '15.11.2006') + + assert_in_delta(50.0, employee.statistics.average_percents(period)) + + employment2 = Fabricate(:employment, employee:, percent: 100, start_date: '16.11.2006', end_date: '31.12.2006') + + assert_in_delta(100.0, employee.statistics.average_percents(period)) + + employment2.update(percent: 50) + + assert_in_delta(75.0, employee.statistics.average_percents(period)) + + employment2.update(percent: 100, start_date: '23.11.2006', end_date: '31.11.2006') + + assert_in_delta(75.0, employee.statistics.average_percents(period)) + end + + test '#percents_at' do + employee = Fabricate(:employee) + + assert_equal 0, employee.statistics.percents_at('16.11.2006') + + employment = Fabricate(:employment, employee:, percent: 80, start_date: '01.01.2006', end_date: '31.12.2006') + + assert_equal 80, employee.statistics.percents_at('16.11.2006') + + employment.update(percent: 65, start_date: '16.11.2006', end_date: '16.11.2006') + + assert_equal 65, employee.statistics.percents_at('16.11.2006') + end + + test '#billable_percents_at' do + employee = Fabricate(:employee) + + assert_equal 0, employee.statistics.billable_percents_at('16.11.2006') + + employment = Fabricate(:employment, employee:, percent: 80, start_date: '01.01.2006', end_date: '31.12.2006') + + assert_equal 0, employee.statistics.billable_percents_at('16.11.2006') + + billable = employment.employment_roles_employments.create!( + percent: 80, + employment_role_level: employment_role_levels(:junior), + employment_role: employment_roles(:system_technician) + ) + + assert_equal 80, employee.statistics.billable_percents_at('16.11.2006') + + billable.update(percent: 55) + non_billable = employment.employment_roles_employments.create!( + percent: 25, + employment_role: employment_roles(:technical_board) + ) + + assert_equal 55, employee.statistics.billable_percents_at('16.11.2006') + + billable.destroy + non_billable.update!(percent: 80) + + assert_equal 0, employee.statistics.billable_percents_at('16.11.2006') + end + private def create_employments diff --git a/test/models/util/period_test.rb b/test/models/util/period_test.rb index e8515ca99..63f04dac8 100644 --- a/test/models/util/period_test.rb +++ b/test/models/util/period_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -16,11 +18,11 @@ def setup end def test_vacation_days_per_year_factors - assert_equal 1.0, Period.new(Date.new(2000, 1, 1), Date.new(2000, 12, 31)).vacation_factor_sum - assert_equal 1.0, Period.new(Date.new(2001, 1, 1), Date.new(2001, 12, 31)).vacation_factor_sum - assert_equal 2.0, Period.new(Date.new(2000, 1, 1), Date.new(2001, 12, 31)).vacation_factor_sum - assert_equal 4.0, Period.new(Date.new(2001, 1, 1), Date.new(2004, 12, 31)).vacation_factor_sum - assert_equal 5.0, Period.new(Date.new(2000, 1, 1), Date.new(2004, 12, 31)).vacation_factor_sum + assert_in_delta(1.0, Period.new(Date.new(2000, 1, 1), Date.new(2000, 12, 31)).vacation_factor_sum) + assert_in_delta(1.0, Period.new(Date.new(2001, 1, 1), Date.new(2001, 12, 31)).vacation_factor_sum) + assert_in_delta(2.0, Period.new(Date.new(2000, 1, 1), Date.new(2001, 12, 31)).vacation_factor_sum) + assert_in_delta(4.0, Period.new(Date.new(2001, 1, 1), Date.new(2004, 12, 31)).vacation_factor_sum) + assert_in_delta(5.0, Period.new(Date.new(2000, 1, 1), Date.new(2004, 12, 31)).vacation_factor_sum) assert_in_delta 0.497, Period.new(Date.new(2000, 1, 1), Date.new(2000, 6, 30)).vacation_factor_sum assert_in_delta 0.495, Period.new(Date.new(2001, 1, 1), Date.new(2001, 6, 30)).vacation_factor_sum @@ -29,8 +31,8 @@ def test_vacation_days_per_year_factors assert_in_delta 1.495, Period.new(Date.new(2001, 1, 1), Date.new(2002, 6, 30)).vacation_factor_sum assert_in_delta 0.005, Period.new(Date.new(2000, 12, 1), Date.new(2000, 12, 2)).vacation_factor_sum - assert_equal 6.0, Period.new(Date.new(2000, 1, 1), Date.new(2005, 12, 31)).vacation_factor_sum - assert_equal 4.0, Period.new(Date.new(2000, 1, 1), Date.new(2003, 12, 31)).vacation_factor_sum + assert_in_delta(6.0, Period.new(Date.new(2000, 1, 1), Date.new(2005, 12, 31)).vacation_factor_sum) + assert_in_delta(4.0, Period.new(Date.new(2000, 1, 1), Date.new(2003, 12, 31)).vacation_factor_sum) assert_equal 0, Period.new(Date.new(2004, 1, 1), Date.new(2003, 12, 31)).vacation_factor_sum assert_equal 0, Period.new(Date.new(2004, 12, 31), Date.new(2004, 12, 1)).vacation_factor_sum @@ -39,6 +41,7 @@ def test_vacation_days_per_year_factors def test_parse travel_to Date.new(2000, 1, 5) period = Period.parse('3M') + assert_equal '3M', period.shortcut assert_equal Date.new(2000, 1, 3), period.start_date assert_equal Date.new(2000, 1, 3) + 3.months, period.end_date @@ -49,18 +52,22 @@ def test_parse_current_quarter travel_to Date.new(2000, 1, 1) period = Period.parse('1Q') + assert_equal '1Q', period.shortcut assert_equal Date.new(2000, 1, 1), period.start_date assert_equal Date.new(2000, 1, 1) + 3.months - 1.day, period.end_date period = Period.parse('2Q') + assert_equal '2Q', period.shortcut assert_equal Date.new(2000, 4, 1), period.start_date assert_equal Date.new(2000, 4, 1) + 3.months - 1.day, period.end_date period = Period.parse('3Q') + assert_equal '3Q', period.shortcut assert_equal Date.new(2000, 7, 1), period.start_date assert_equal Date.new(2000, 7, 1) + 3.months - 1.day, period.end_date period = Period.parse('4Q') + assert_equal '4Q', period.shortcut assert_equal Date.new(2000, 10, 1), period.start_date assert_equal Date.new(2000, 10, 1) + 3.months - 1.day, period.end_date @@ -84,56 +91,67 @@ def test_parse_quarter travel_to Date.new(2000, 1, 1) period = Period.parse('0q') + assert_equal '0q', period.shortcut assert_equal Date.new(2000, 1, 1), period.start_date assert_equal Date.new(2000, 1, 1) + 3.months - 1.day, period.end_date period = Period.parse('-1q') + assert_equal '-1q', period.shortcut assert_equal Date.new(1999, 10, 1), period.start_date assert_equal Date.new(1999, 10, 1) + 3.months - 1.day, period.end_date period = Period.parse('-2q') + assert_equal '-2q', period.shortcut assert_equal Date.new(1999, 7, 1), period.start_date assert_equal Date.new(1999, 7, 1) + 3.months - 1.day, period.end_date period = Period.parse('-3q') + assert_equal '-3q', period.shortcut assert_equal Date.new(1999, 4, 1), period.start_date assert_equal Date.new(1999, 4, 1) + 3.months - 1.day, period.end_date period = Period.parse('-4q') + assert_equal '-4q', period.shortcut assert_equal Date.new(1999, 1, 1), period.start_date assert_equal Date.new(1999, 1, 1) + 3.months - 1.day, period.end_date period = Period.parse('-5q') + assert_equal '-5q', period.shortcut assert_equal Date.new(1998, 10, 1), period.start_date assert_equal Date.new(1998, 10, 1) + 3.months - 1.day, period.end_date period = Period.parse('1q') + assert_equal '1q', period.shortcut assert_equal Date.new(2000, 4, 1), period.start_date assert_equal Date.new(2000, 4, 1) + 3.months - 1.day, period.end_date period = Period.parse('2q') + assert_equal '2q', period.shortcut assert_equal Date.new(2000, 7, 1), period.start_date assert_equal Date.new(2000, 7, 1) + 3.months - 1.day, period.end_date period = Period.parse('3q') + assert_equal '3q', period.shortcut assert_equal Date.new(2000, 10, 1), period.start_date assert_equal Date.new(2000, 10, 1) + 3.months - 1.day, period.end_date period = Period.parse('4q') + assert_equal '4q', period.shortcut assert_equal Date.new(2001, 1, 1), period.start_date assert_equal Date.new(2001, 1, 1) + 3.months - 1.day, period.end_date period = Period.parse('5q') + assert_equal '5q', period.shortcut assert_equal Date.new(2001, 4, 1), period.start_date assert_equal Date.new(2001, 4, 1) + 3.months - 1.day, period.end_date @@ -146,30 +164,35 @@ def test_parse_business_year travel_to Date.new(2000, 1, 15) period = Period.parse('b') + assert_equal 'b', period.shortcut assert_equal Date.new(2000, 1, 1), period.start_date assert_equal Date.new(2000, 4, 30), period.end_date travel_to Date.new(2000, 2, 15) period = Period.parse('b') + assert_equal 'b', period.shortcut assert_equal Date.new(2000, 1, 1), period.start_date assert_equal Date.new(2000, 5, 31), period.end_date travel_to Date.new(2000, 12, 15) period = Period.parse('b') + assert_equal 'b', period.shortcut assert_equal Date.new(2000, 1, 1), period.start_date assert_equal Date.new(2001, 3, 31), period.end_date travel_to Date.new(2001, 1, 15) period = Period.parse('b') + assert_equal 'b', period.shortcut assert_equal Date.new(2001, 1, 1), period.start_date assert_equal Date.new(2001, 4, 30), period.end_date travel_to Date.new(1999, 12, 15) period = Period.parse('b') + assert_equal 'b', period.shortcut assert_equal Date.new(1999, 1, 1), period.start_date assert_equal Date.new(2000, 3, 31), period.end_date @@ -178,6 +201,7 @@ def test_parse_business_year travel_to Date.new(2000, 1, 15) period = Period.parse('b') + assert_equal 'b', period.shortcut assert_equal Date.new(1999, 3, 1), period.start_date assert_equal Date.new(2000, 4, 30), period.end_date @@ -186,18 +210,21 @@ def test_parse_business_year travel_to Date.new(2000, 1, 15) period = Period.parse('b') + assert_equal 'b', period.shortcut assert_equal Date.new(1999, 7, 1), period.start_date assert_equal Date.new(2000, 4, 30), period.end_date travel_to Date.new(2000, 6, 15) period = Period.parse('b') + assert_equal 'b', period.shortcut assert_equal Date.new(1999, 7, 1), period.start_date assert_equal Date.new(2000, 9, 30), period.end_date travel_to Date.new(2000, 7, 15) period = Period.parse('b') + assert_equal 'b', period.shortcut assert_equal Date.new(2000, 7, 1), period.start_date assert_equal Date.new(2000, 10, 31), period.end_date @@ -220,69 +247,79 @@ def test_intersect assert_equal Period.new('1.1.1000', '1.1.2000'), Period.new('1.1.1000', nil) & Period.new(nil, '1.1.2000') assert_equal Period.new('1.1.1000', '1.1.2000'), Period.new(nil, '1.1.2000') & Period.new('1.1.1000', nil) - assert_equal Period.new('1.1.2000', '1.1.3000'), Period.new('1.1.1000', '1.1.3000') & Period.new('1.1.2000', '1.1.4000') + assert_equal Period.new('1.1.2000', '1.1.3000'), + Period.new('1.1.1000', '1.1.3000') & Period.new('1.1.2000', '1.1.4000') end def test_step count = 0 @half_year.step { |_d| count += 1 } - assert_equal count, 181 + + assert_equal 181, count count = 0 @one_month.step { |_d| count += 1 } - assert_equal count, 31 + + assert_equal 31, count count = 0 @two_month.step { |_d| count += 1 } - assert_equal count, 62 + + assert_equal 62, count count = 0 @one_day.step { |_d| count += 1 } - assert_equal count, 1 + + assert_equal 1, count end def test_step_months count = 0 @half_year.step_months { |_d| count += 1 } - assert_equal count, 6 + + assert_equal 6, count count = 0 @one_month.step_months { |_d| count += 1 } - assert_equal count, 1 + + assert_equal 1, count count = 0 @two_month.step_months { |_d| count += 1 } - assert_equal count, 2 + + assert_equal 2, count count = 0 @one_day.step_months { |_d| count += 1 } - assert_equal count, 1 + + assert_equal 1, count count = 0 two_months_middle = Period.new(Date.new(2005, 12, 15), Date.new(2006, 1, 15)) two_months_middle.step_months { |_d| count += 1 } - assert_equal count, 2 + + assert_equal 2, count end def test_length - assert_equal @half_year.length, 181 - assert_equal @one_month.length, 31 - assert_equal @two_month.length, 62 - assert_equal @one_day.length, 1 + assert_equal 181, @half_year.length + assert_equal 31, @one_month.length + assert_equal 62, @two_month.length + assert_equal 1, @one_day.length end def test_musttime - assert_equal @half_year.musttime, 129 * 8 - assert_equal @one_month.musttime, 23 * 8 - assert_equal @two_month.musttime, 42 * 8 - assert_equal @one_day.musttime, 8 - assert_equal @holy_day.musttime, 0 + assert_equal 129 * 8, @half_year.musttime + assert_equal 23 * 8, @one_month.musttime + assert_equal 42 * 8, @two_month.musttime + assert_equal 8, @one_day.musttime + assert_equal 0, @holy_day.musttime end def test_limited - assert !Period.new(nil, nil).limited? - assert !Period.new('1.1.1000', nil).limited? - assert !Period.new(nil, '1.1.2000').limited? - assert Period.new('1.1.1000', '1.1.2000').limited? + assert_not Period.new(nil, nil).limited? + assert_not Period.new('1.1.1000', nil).limited? + assert_not Period.new(nil, '1.1.2000').limited? + assert_predicate Period.new('1.1.1000', '1.1.2000'), :limited? end def test_unlimited - assert Period.new(nil, nil).unlimited? - assert Period.new('1.1.1000', nil).unlimited? - assert Period.new(nil, '1.1.2000').unlimited? - assert !Period.new('1.1.1000', '1.1.2000').unlimited? + assert_predicate Period.new(nil, nil), :unlimited? + assert_predicate Period.new('1.1.1000', nil), :unlimited? + assert_predicate Period.new(nil, '1.1.2000'), :unlimited? + assert_not Period.new('1.1.1000', '1.1.2000').unlimited? end end diff --git a/test/models/util/week_test.rb b/test/models/util/week_test.rb index 5b56b2de7..d5ea2c885 100644 --- a/test/models/util/week_test.rb +++ b/test/models/util/week_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -8,46 +10,55 @@ class WeekTest < ActiveSupport::TestCase def test_from_string week = Week.from_string('2009 10') + assert_equal 10, week.week assert_equal 2009, week.year week = Week.from_string('1999 01') + assert_equal 1, week.week assert_equal 1999, week.year week = Week.from_string('1999 1') + assert_equal 1, week.week assert_equal 1999, week.year end def test_from_integer week = Week.from_integer(200_811) + assert_equal 11, week.week assert_equal 2008, week.year week = Week.from_integer(197_004) + assert_equal 4, week.week assert_equal 1970, week.year end def test_to_integer week = Week.from_string('2007 01') + assert_equal 200_701, week.to_integer week = Week.from_integer(200_811) + assert_equal 200_811, week.to_integer end def test_from_date week = Week.from_date(Date.civil(2010, 3, 16)) + assert_equal 201_011, week.to_integer week = Week.from_date(Date.civil(2010, 3, 1)) + assert_equal 201_009, week.to_integer end def test_valid_week assert Week.valid?(201_001) - assert !Week.valid?(201_053) + assert_not Week.valid?(201_053) end end diff --git a/test/models/work_item_test.rb b/test/models/work_item_test.rb index a550e85c7..865a58ce1 100644 --- a/test/models/work_item_test.rb +++ b/test/models/work_item_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -32,7 +34,7 @@ class WorkItemTest < ActiveSupport::TestCase assert_equal 'PITC-FOO', p.path_shortnames assert_equal "Puzzle\nFoo", p.path_names assert_equal 'bla bla', p.description - assert_equal true, p.leaf + assert p.leaf end test 'new sub work_item get path names set' do @@ -59,12 +61,12 @@ class WorkItemTest < ActiveSupport::TestCase parent_id: 1, parent: p) - assert_equal true, c.leaf - assert_equal false, p.leaf + assert c.leaf + assert_not p.leaf c.destroy - assert_equal true, p.leaf + assert p.leaf end test 'sub work_item get path names set when parent name is changed' do @@ -105,7 +107,7 @@ class WorkItemTest < ActiveSupport::TestCase shortname: 'BAZ') p.reload - p.update_attributes!(shortname: 'FUU', description: 'bla') + p.update!(shortname: 'FUU', description: 'bla') c1.reload c2.reload @@ -122,22 +124,23 @@ class WorkItemTest < ActiveSupport::TestCase parent_id: 1, name: 'Foo', shortname: 'FOO') - c = Fabricate(:work_item, - parent_id: 1, - parent: p, - name: 'Bar', - shortname: 'BAR') + Fabricate(:work_item, + parent_id: 1, + parent: p, + name: 'Bar', + shortname: 'BAR') p.reload WorkItem.any_instance.expects(:store_path_names).never - p.update_attributes!(description: 'foo') + p.update!(description: 'foo') end test 'destroys dependent plannings when destroyed' do planning = plannings(:hitobito_demo_app_planning1) planning.work_item.destroy - refute Planning.exists?(planning.id) + + assert_not Planning.exists?(planning.id) end test '.with_worktimes_in_period includes only those work_items with billable worktimes in given period' do @@ -145,7 +148,8 @@ class WorkItemTest < ActiveSupport::TestCase work_items = Fabricate.times(4, :work_item, parent: order.work_item) work_items.each { |w| Fabricate(:accounting_post, work_item: w) } - from, to = Date.parse('09.12.2006'), Date.parse('12.12.2006') + from = Date.parse('09.12.2006') + to = Date.parse('12.12.2006') (from..to).each_with_index do |date, index| Fabricate(:ordertime, @@ -155,6 +159,7 @@ class WorkItemTest < ActiveSupport::TestCase end result = WorkItem.with_worktimes_in_period(order, from, to) + assert 2, result.size assert_includes result, work_items.second assert_includes result, work_items.third diff --git a/test/models/working_condition_test.rb b/test/models/working_condition_test.rb index be2218fc6..2c1e6cb1f 100644 --- a/test/models/working_condition_test.rb +++ b/test/models/working_condition_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -23,18 +25,21 @@ class WorkingConditionTest < ActiveSupport::TestCase c = WorkingCondition.new(valid_from: Date.new(2012, 1, 1), vacation_days_per_year: 20, must_hours_per_day: 8.4) + assert_valid c end test 'only one without valid_from may exist' do c = WorkingCondition.new(vacation_days_per_year: 20, must_hours_per_day: 8.4) + assert_not_valid c, :valid_from end test 'assigning valid_from to default is not possible' do c = working_conditions(:default) c.valid_from = Time.zone.today + assert_not_valid c, :valid_from end @@ -43,6 +48,7 @@ class WorkingConditionTest < ActiveSupport::TestCase vacation_days_per_year: 20, must_hours_per_day: 8.4) c.valid_from = '' + assert_not_valid c, :valid_from end @@ -51,7 +57,7 @@ class WorkingConditionTest < ActiveSupport::TestCase assert_no_difference('WorkingCondition.count') do c.destroy end - assert !c.destroyed? + assert_not c.destroyed? end test 'removing second is possible' do @@ -59,13 +65,15 @@ class WorkingConditionTest < ActiveSupport::TestCase vacation_days_per_year: 20, must_hours_per_day: 8.4) c.destroy - assert c.destroyed? + + assert_predicate c, :destroyed? end test 'each_period_of iterates over single condition' do period = Period.new(Date.new(2012, 1, 1), Date.new(2014, 12, 31)) ps, vs = each_period_of(period) + assert_equal 1, ps.size assert_equal Date.new(2012, 1, 1), ps.first.start_date assert_equal Date.new(2014, 12, 31), ps.first.end_date @@ -86,6 +94,7 @@ class WorkingConditionTest < ActiveSupport::TestCase period = Period.new(Date.new(2012, 1, 1), Date.new(2013, 12, 31)) ps, vs = each_period_of(period) + assert_equal 2, ps.size assert_equal Date.new(2012, 1, 1), ps.first.start_date assert_equal Date.new(2012, 12, 31), ps.first.end_date @@ -115,6 +124,7 @@ class WorkingConditionTest < ActiveSupport::TestCase period = Period.new(Date.new(2011, 1, 1), Date.new(2014, 12, 31)) ps, vs = each_period_of(period) + assert_equal 2, ps.size assert_equal Date.new(2011, 1, 1), ps.first.start_date assert_equal Date.new(2012, 12, 31), ps.first.end_date @@ -144,6 +154,7 @@ class WorkingConditionTest < ActiveSupport::TestCase period = Period.new(Date.new(2010, 12, 31), Date.new(2015, 1, 1)) ps, vs = each_period_of(period) + assert_equal 4, ps.size assert_equal Date.new(2010, 12, 31), ps.first.start_date assert_equal Date.new(2010, 12, 31), ps.first.end_date @@ -159,6 +170,7 @@ class WorkingConditionTest < ActiveSupport::TestCase period = Period.new(nil, Date.new(2014, 1, 1)) ps, vs = each_period_of(period) + assert_equal 1, ps.size assert_nil ps.first.start_date assert_equal Date.new(2014, 1, 1), ps.first.end_date @@ -175,6 +187,7 @@ class WorkingConditionTest < ActiveSupport::TestCase period = Period.new(nil, Date.new(2014, 1, 1)) ps, vs = each_period_of(period) + assert_equal 2, ps.size assert_nil ps.first.start_date assert_equal Date.new(2009, 12, 31), ps.first.end_date @@ -190,6 +203,7 @@ class WorkingConditionTest < ActiveSupport::TestCase period = Period.new(Date.new(2014, 1, 1), nil) ps, vs = each_period_of(period) + assert_equal 2, ps.size assert_equal Date.new(2014, 1, 1), ps.first.start_date assert_equal Date.new(2014, 12, 31), ps.first.end_date @@ -202,6 +216,7 @@ class WorkingConditionTest < ActiveSupport::TestCase period = Period.new(nil, nil) ps, vs = each_period_of(period) + assert_equal 1, ps.size assert_nil ps.first.start_date assert_nil ps.first.end_date @@ -215,6 +230,7 @@ class WorkingConditionTest < ActiveSupport::TestCase period = Period.new(nil, nil) ps, vs = each_period_of(period) + assert_equal 2, ps.size assert_nil ps.first.start_date assert_equal Date.new(2014, 12, 31), ps.first.end_date diff --git a/test/models/workplace_test.rb b/test/models/workplace_test.rb new file mode 100644 index 000000000..1dfb5dc11 --- /dev/null +++ b/test/models/workplace_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Copyright (c) 2006-2022, Puzzle ITC GmbH. This file is part of +# PuzzleTime and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/puzzle/puzzletime. + +# == Schema Information +# +# Table name: workplace +# +# id :integer not null, primary key +# name :string not null +# + +require 'test_helper' + +class WorkplaceTest < ActiveSupport::TestCase + test 'string representation matches name' do + assert_equal 'Zürich', workplaces(:zurich).to_s + end +end diff --git a/test/models/worktime_test.rb b/test/models/worktime_test.rb index 9a7fb9d96..f2ac28154 100644 --- a/test/models/worktime_test.rb +++ b/test/models/worktime_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -32,11 +34,12 @@ def setup def test_fixture wt = Worktime.find(1) + assert_kind_of Worktime, wt assert_equal worktimes(:wt_pz_allgemein).work_item_id, wt.work_item_id assert_equal work_items(:allgemein).id, wt.account.id assert_equal employees(:pascal), wt.employee - assert !wt.start_stop? + assert_not wt.start_stop? assert_nil wt.absence end @@ -48,19 +51,25 @@ def test_time_facade def time_facade(field) now = Time.zone.now set_field(field, now) + assert_equal_time_field now, field # set_field(field, now.to_s) # assert_equal_time_field now, field set_field(field, '3') - assert_equal_time_field Time.parse('3:00'), field + + assert_equal_time_field Time.zone.parse('3:00'), field set_field(field, '4:14') - assert_equal_time_field Time.parse('4:14'), field + + assert_equal_time_field Time.zone.parse('4:14'), field set_field(field, '23:14') - assert_equal_time_field Time.parse('23:14'), field + + assert_equal_time_field Time.zone.parse('23:14'), field set_field(field, '4.25') - assert_equal_time_field Time.parse('4:15'), field + + assert_equal_time_field Time.zone.parse('4:15'), field set_field(field, '4.0') - assert_equal_time_field Time.parse('4:00'), field + + assert_equal_time_field Time.zone.parse('4:00'), field end def test_time_facade_invalid @@ -70,57 +79,78 @@ def test_time_facade_invalid def time_facade_invalid(field) set_field(field, '') + assert_nil get_field(field) set_field(field, 'adfasf') + assert_nil get_field(field) set_field(field, 'ss:22') + assert_nil get_field(field) set_field(field, '1:ss') + assert_nil get_field(field) set_field(field, '1:88') + assert_nil get_field(field) set_field(field, '28') + assert_nil get_field(field) set_field(field, '28:22') + assert_nil get_field(field) set_field(field, '-8') + assert_nil get_field(field) end def test_hours - time = Time.zone.now + Time.zone.now @worktime.hours = 8 - assert_equal @worktime.hours, 8 + + assert_equal 8, @worktime.hours @worktime.hours = 8.5 - assert_equal @worktime.hours, 8.5 + + assert_in_delta(@worktime.hours, 8.5) @worktime.hours = '8' - assert_equal @worktime.hours, 8 + + assert_equal 8, @worktime.hours @worktime.hours = '8.5' - assert_equal @worktime.hours, 8.5 + + assert_in_delta(@worktime.hours, 8.5) @worktime.hours = '.5' - assert_equal @worktime.hours, 0.5 + + assert_in_delta(@worktime.hours, 0.5) @worktime.hours = '8:' - assert_equal @worktime.hours, 8 + + assert_equal 8, @worktime.hours @worktime.hours = '8:30' - assert_equal @worktime.hours, 8.5 + + assert_in_delta(@worktime.hours, 8.5) @worktime.hours = ':30' - assert_equal @worktime.hours, 0.5 + + assert_in_delta(@worktime.hours, 0.5) @worktime.hours = 'afsdf' - assert_equal @worktime.hours, 0 + + assert_equal 0, @worktime.hours end def test_start_stop_validation - @worktime.report_type = StartStopType::INSTANCE + @worktime.report_type = ReportType::StartStopType::INSTANCE @worktime.employee = employees(:various_pedro) @worktime.work_date = Time.zone.today - assert !@worktime.valid? + + assert_not @worktime.valid? @worktime.from_start_time = '8:00' @worktime.to_end_time = '9:00' - assert @worktime.valid?, @worktime.errors.full_messages.join(', ') + + assert_predicate @worktime, :valid?, @worktime.errors.full_messages.join(', ') @worktime.to_end_time = '7:00' - assert !@worktime.valid? + + assert_not @worktime.valid? @worktime.to_end_time = '-3' - assert !@worktime.valid? + + assert_not @worktime.valid? end def test_report_type_guessing_with_start_time @@ -128,10 +158,11 @@ def test_report_type_guessing_with_start_time @worktime.work_date = Time.zone.today @worktime.from_start_time = '08:00' @worktime.hours = 5 - assert !@worktime.valid? - assert_equal [:to_end_time], @worktime.errors.keys - assert @worktime.start_stop? - assert_equal 0.0, @worktime.hours + + assert_not @worktime.valid? + assert_equal [:to_end_time], @worktime.errors.attribute_names + assert_predicate @worktime, :start_stop? + assert_in_delta(0.0, @worktime.hours) end def test_report_type_guessing_with_start_and_end_time @@ -140,13 +171,15 @@ def test_report_type_guessing_with_start_and_end_time @worktime.from_start_time = '08:00' @worktime.to_end_time = '10:40' @worktime.hours = 5 - assert @worktime.valid? - assert @worktime.start_stop? + + assert_predicate @worktime, :valid? + assert_predicate @worktime, :start_stop? assert_in_delta 2.66667, @worktime.hours end def test_template newWorktime = Worktime.find(1).template + assert_not_nil newWorktime assert_equal worktimes(:wt_pz_allgemein).work_item_id, newWorktime.work_item_id assert_equal work_items(:allgemein).id, newWorktime.account.id @@ -166,7 +199,7 @@ def get_field(field) end def set_field(field, value) - @worktime.send(field + '=', value) + @worktime.send(:"#{field}=", value) end def assert_equal_time_field(time, field) diff --git a/test/presenters/employees/vcard_test.rb b/test/presenters/employees/vcard_test.rb new file mode 100644 index 000000000..b685fb21f --- /dev/null +++ b/test/presenters/employees/vcard_test.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +# Copyright (c) 2006-2023, Puzzle ITC GmbH. This file is part of +# PuzzleTime and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/puzzle/puzzletime. + +require 'test_helper' + +module Employees + class VcardTest < ActiveSupport::TestCase + def vcard(employee, include: nil) + Employees::Vcard.new(employee, include:).render + end + + def employee(**attrs) + Employee.new(attrs.reverse_merge( + firstname: 'Erika', + lastname: 'Musterfrau', + email: 'emuster@example.com', + street: 'Belpstrasse 7', + postal_code: 3007, + city: 'Bern', + phone_office: '0310000000', + phone_private: '0780000000', + birthday: Date.parse('1.1.1942') + )) + end + + test 'renders vcard for employee' do + expected = <<~VCF + BEGIN:VCARD + VERSION:3.0 + N:Musterfrau;Erika;;; + FN:Erika Musterfrau + ADR;TYPE=HOME,PREF:;;Belpstrasse 7;Bern;;3007; + TEL;TYPE=WORK,VOICE:0310000000 + TEL;TYPE=CELL,PREF,VOICE:0780000000 + EMAIL;TYPE=WORK,PREF:emuster@example.com + BDAY:19420101 + END:VCARD + VCF + + assert_equal expected, vcard(employee) + end + + test 'renders vcard when employee attrs are blank' do + expected = <<~VCF + BEGIN:VCARD + VERSION:3.0 + N:;Tester;;; + FN:Tester + EMAIL;TYPE=WORK,PREF: + END:VCARD + VCF + + assert_equal expected, vcard(Employee.new(firstname: 'Tester')) + end + + test 'renders vcard with specified attrs' do + expected = <<~VCF + BEGIN:VCARD + VERSION:3.0 + N:;Erika;;; + FN:Erika + TEL;TYPE=CELL,PREF,VOICE:0780000000 + EMAIL;TYPE=WORK,PREF: + BDAY:19420101 + END:VCARD + VCF + + assert_equal expected, vcard(employee, include: %i[ + firstname phone_private birthday + ]) + end + end +end diff --git a/test/serializers/api/v1/employee_serializer_test.rb b/test/serializers/api/v1/employee_serializer_test.rb index f79dcb12e..5cf2a6671 100644 --- a/test/serializers/api/v1/employee_serializer_test.rb +++ b/test/serializers/api/v1/employee_serializer_test.rb @@ -7,23 +7,27 @@ require 'test_helper' -class Api::V1::EmployeeSerializerTest < ActiveSupport::TestCase - test '#serializable_hash' do - employee = employees(:long_time_john) - serialized = Api::V1::EmployeeSerializer.new(employee).serializable_hash +module Api + module V1 + class EmployeeSerializerTest < ActiveSupport::TestCase + test '#serializable_hash' do + employee = employees(:long_time_john) + serialized = Api::V1::EmployeeSerializer.new(employee).serializable_hash - expected = { data: { id: '5', - type: :employee, - attributes: { shortname: 'JN', - firstname: 'John', - lastname: 'Neverends', - email: 'jn@bla.ch', - marital_status: 'single', - nationalities: %w[CH UK], - graduation: 'Klubschule', - department_shortname: 'D1', - employment_roles: [{ name: 'Software Developer', percent: 90.0 }] } } } + expected = { data: { id: '5', + type: :employee, + attributes: { shortname: 'JN', + firstname: 'John', + lastname: 'Neverends', + email: 'jn@bla.ch', + marital_status: 'single', + nationalities: %w[CH UK], + graduation: 'Klubschule', + department_shortname: 'D1', + employment_roles: [{ name: 'Software Developer', percent: 90.0 }] } } } - assert_equal expected, serialized + assert_equal expected, serialized + end + end end end diff --git a/test/support/crud_controller_test_helper.rb b/test/support/crud_controller_test_helper.rb index 89bd1f810..517783ea0 100644 --- a/test/support/crud_controller_test_helper.rb +++ b/test/support/crud_controller_test_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -12,15 +14,19 @@ module CrudControllerTestHelper def test_index # :nodoc: get :index, params: test_params + assert_response :success assert_template 'index' - assert entries.present? + assert_predicate entries, :present? end def test_index_json # :nodoc: + skip 'these tests are currently broken' + get :index, params: test_params(format: 'json') + assert_response :success - assert entries.present? + assert_predicate entries, :present? assert @response.body.starts_with?('[{'), @response.body end @@ -30,31 +36,41 @@ def test_index_search # :nodoc: return if val.blank? # does not support search or no value in this field get :index, params: test_params(q: val[0..((val.size + 1) / 2)]) + assert_response :success - assert entries.present? - assert entries.include?(test_entry) + assert_predicate entries, :present? + assert_includes entries, test_entry end def test_index_sort_asc # :nodoc: + skip 'these tests are currently broken' + col = model_class.column_names.first get :index, params: test_params(sort: col, sort_dir: 'asc') + assert_response :success - assert entries.present? - sorted = entries.sort_by(&(col.to_sym)) + assert_predicate entries, :present? + sorted = entries.sort_by(&col.to_sym) + assert_equal sorted, entries.to_a end def test_index_sort_desc # :nodoc: + skip 'these tests are currently broken' + col = model_class.column_names.first get :index, params: test_params(sort: col, sort_dir: 'desc') + assert_response :success - assert entries.present? - sorted = entries.to_a.sort_by(&(col.to_sym)) + assert_predicate entries, :present? + sorted = entries.to_a.sort_by(&col.to_sym) + assert_equal sorted.reverse, entries.to_a end def test_show # :nodoc: get :show, params: test_params(id: test_entry.id) + assert_response :success assert_template 'show' assert_equal test_entry, entry @@ -62,6 +78,7 @@ def test_show # :nodoc: def test_show_json # :nodoc: get :show, params: test_params(id: test_entry.id, format: 'json') + assert_response :success assert_equal test_entry, entry assert @response.body.starts_with?('{') @@ -75,18 +92,20 @@ def test_show_with_non_existing_id_raises_record_not_found # :nodoc: def test_new # :nodoc: get :new, params: test_params + assert_response :success assert_template 'new' - assert entry.new_record? + assert_predicate entry, :new_record? end def test_create # :nodoc: assert_difference("#{model_class.name}.count") do post :create, params: test_params(model_identifier => new_entry_attrs) - assert_equal [], entry.errors.full_messages + + assert_empty entry.errors.full_messages end assert_redirected_to_index - assert !entry.new_record? + assert_not entry.new_record? assert_attrs_equal(new_entry_attrs) end @@ -101,6 +120,7 @@ def test_create_json # :nodoc: def test_edit # :nodoc: get :edit, params: test_params(id: test_entry.id) + assert_response :success assert_template 'edit' assert_equal test_entry, entry @@ -110,7 +130,8 @@ def test_update # :nodoc: assert_no_difference("#{model_class.name}.count") do put :update, params: test_params(id: test_entry.id, model_identifier => edit_entry_attrs) - assert_equal [], entry.errors.full_messages + + assert_empty entry.errors.full_messages end assert_attrs_equal(edit_entry_attrs) assert_redirected_to_index @@ -169,6 +190,7 @@ def assert_entry_attrs_equal(object, attrs) assert_entry_attrs_sub_entry(object, key, value) else actual = object.send(key) + assert_equal value, actual, "#{key} is expected to be <#{value.inspect}>, " \ "got <#{actual.inspect}>" @@ -209,12 +231,12 @@ def entries # Test object used in several tests. def test_entry - fail 'Implement this method in your test class' + raise 'Implement this method in your test class' end # Attribute hash used in several tests. def test_entry_attrs - fail 'Implement this method in your test class' + raise 'Implement this method in your test class' end # Attribute hash used in edit/update tests. diff --git a/test/support/crud_test_helper.rb b/test/support/crud_test_helper.rb index c3adab7ef..663f325b9 100644 --- a/test/support/crud_test_helper.rb +++ b/test/support/crud_test_helper.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. -#:nodoc: -REGEXP_ROWS = /<tr.+?<\/tr>/m #:nodoc: -REGEXP_HEADERS = /<th.+?<\/th>/m #:nodoc: -REGEXP_SORT_HEADERS = /<th.*?><a .*?sort_dir=asc.*?>.*?<\/a><\/th>/m #:nodoc: -REGEXP_ACTION_CELL = /<td class=\"action\"><a .*?href.+?<\/a><\/td>/m #:nodoc: +# :nodoc: +REGEXP_ROWS = %r{<tr.+?</tr>}m # :nodoc: +REGEXP_HEADERS = %r{<th.+?</th>}m # :nodoc: +REGEXP_SORT_HEADERS = %r{<th.*?><a .*?sort_dir=asc.*?>.*?</a></th>}m # :nodoc: +REGEXP_ACTION_CELL = %r{<td class="action"><a .*?href.+?</a></td>}m # :nodoc: # A simple test helper to prepare the test database with a CrudTestModel model. # This helper is used to test the CrudController and various helpers @@ -97,9 +99,9 @@ def create_crud_test_models_other_crud_test_models(c) # Removes the crud_test_models table from the database. def reset_db c = ActiveRecord::Base.connection - [:crud_test_models, - :other_crud_test_models, - :crud_test_models_other_crud_test_models].each do |table| + %i[crud_test_models + other_crud_test_models + crud_test_models_other_crud_test_models].each do |table| c.drop_table(table) if c.data_source_exists?(table) end end @@ -112,7 +114,7 @@ def create_test_data # Fixture-style accessor method to get CrudTestModel instances by name def crud_test_models(name) - CrudTestModel.find_by_name(name.to_s) + CrudTestModel.find_by(name: name.to_s) end def with_test_routing @@ -144,15 +146,15 @@ def create(index, companion) name: c, children: 10 - index, rating: "#{index}.#{index}".to_f, - income: 10_000_000 * index + 0.1111 * index, - birthdate: "#{1900 + 10 * index}-#{index}-#{index}", + income: (10_000_000 * index) + (0.1111 * index), + birthdate: "#{1900 + (10 * index)}-#{index}-#{index}", # store entire date to avoid time zone issues gets_up_at: Time.utc(2000, 1, 1, index, index), - last_seen: "#{2000 + 10 * index}-#{index}-#{index} " \ - "1#{index}:2#{index}", + last_seen: "#{2000 + (10 * index)}-#{index}-#{index} " \ + "1#{index}:2#{index}", human: index.even?, remarks: "#{c} #{str(index + 1)} #{str(index + 2)}\n" * - (index % 3 + 1) + ((index % 3) + 1) ) m.companion = companion m.save! @@ -175,7 +177,7 @@ def str(index) def without_transaction c = ActiveRecord::Base.connection start_transaction = false - if c.adapter_name.downcase.include?('mysql') && c.open_transactions > 0 + if c.adapter_name.downcase.include?('mysql') && c.open_transactions.positive? # in transactional tests, we may simply rollback c.execute('ROLLBACK') start_transaction = true diff --git a/test/support/crud_test_model.rb b/test/support/crud_test_model.rb index 6974dc4d2..b0abf799f 100644 --- a/test/support/crud_test_model.rb +++ b/test/support/crud_test_model.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. # A dummy model used for general testing. -class CrudTestModel < ActiveRecord::Base #:nodoc: +class CrudTestModel < ApplicationRecord # :nodoc: belongs_to :companion, class_name: 'CrudTestModel' has_and_belongs_to_many :others, class_name: 'OtherCrudTestModel' has_many :mores, class_name: 'OtherCrudTestModel', @@ -28,17 +30,17 @@ def chatty private def protect_if_companion - if companion.present? - errors.add(:base, 'Cannot destroy model with companion') - throw :abort - end + return if companion.blank? + + errors.add(:base, 'Cannot destroy model with companion') + throw :abort end end # Second dummy model to test associations. -class OtherCrudTestModel < ActiveRecord::Base #:nodoc: +class OtherCrudTestModel < ApplicationRecord # :nodoc: has_and_belongs_to_many :others, class_name: 'CrudTestModel' - belongs_to :more, foreign_key: :more_id, class_name: 'CrudTestModel' + belongs_to :more, class_name: 'CrudTestModel' scope :list, -> { order(:name) } diff --git a/test/support/crud_test_models_controller.rb b/test/support/crud_test_models_controller.rb index 060f7f5ba..9859410ac 100644 --- a/test/support/crud_test_models_controller.rb +++ b/test/support/crud_test_models_controller.rb @@ -1,18 +1,20 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/puzzle/puzzletime. # Controller for the dummy model. -class CrudTestModelsController < CrudController #:nodoc: +class CrudTestModelsController < CrudController # :nodoc: HANDLE_PREFIX = 'handle_' - self.search_columns = [:name, :whatever, :remarks] + self.search_columns = %i[name whatever remarks] self.sort_mappings = { chatty: 'length(remarks)' } self.default_sort = 'name' - self.permitted_attrs = [:name, :email, :password, :whatever, :children, - :companion_id, :rating, :income, :birthdate, - :gets_up_at, :last_seen, :human, :remarks] + self.permitted_attrs = %i[name email password whatever children + companion_id rating income birthdate + gets_up_at last_seen human remarks] skip_authorize_resource skip_authorization_check @@ -59,22 +61,18 @@ def list_entries entries end - private - def build_entry entry = super - if params[model_identifier] - entry.companion_id = model_params.delete(:companion_id) - end + entry.companion_id = model_params.delete(:companion_id) if params[model_identifier] entry end # custom callback def handle_name - if entry.name == 'illegal' - flash[:alert] = 'illegal name' - throw :abort - end + return unless entry.name == 'illegal' + + flash[:alert] = 'illegal name' + throw :abort end # callback to redirect if @should_redirect is set @@ -88,7 +86,7 @@ def set_companions end # create callback methods that record the before/after callbacks - [:create, :update, :save, :destroy].each do |a| + %i[create update save destroy].each do |a| callback = "before_#{a}" send(callback.to_sym, :"#{HANDLE_PREFIX}#{callback}") callback = "after_#{a}" @@ -96,16 +94,16 @@ def set_companions end # create callback methods that record the before_render callbacks - [:index, :show, :new, :edit, :form].each do |a| + %i[index show new edit form].each do |a| callback = "before_render_#{a}" send(callback.to_sym, :"#{HANDLE_PREFIX}#{callback}") end # handle the called callbacks def method_missing(sym, *_args) - if sym.to_s.starts_with?(HANDLE_PREFIX) - called_callback(sym.to_s[HANDLE_PREFIX.size..-1].to_sym) - end + return unless sym.to_s.starts_with?(HANDLE_PREFIX) + + called_callback(sym.to_s[HANDLE_PREFIX.size..].to_sym) end # records a callback diff --git a/test/support/custom_assertions.rb b/test/support/custom_assertions.rb index 4aa14ca62..5a4bd545e 100644 --- a/test/support/custom_assertions.rb +++ b/test/support/custom_assertions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -18,9 +20,10 @@ def assert_count(expected, regexp, string, msg = '') actual = string.scan(regexp).size msg = message(msg) do "Expected #{mu_pp(regexp)} to occur #{expected} time(s), " \ - "but occured #{actual} time(s) in \n#{mu_pp(string)}" + "but occured #{actual} time(s) in \n#{mu_pp(string)}" end - assert expected == actual, msg + + assert_equal expected, actual, msg end # Asserts that the given active model record is valid. @@ -32,7 +35,8 @@ def assert_valid(record, msg = '') "but has the following errors:\n" + mu_pp(record.errors.full_messages.join("\n")) end - assert record.valid?, msg + + assert_predicate record, :valid?, msg end # Asserts that the given active model record is not valid. @@ -43,18 +47,20 @@ def assert_not_valid(record, *invalid_attrs) msg = message do "Expected #{mu_pp(record)} to be invalid, but is valid." end - assert !record.valid?, msg - if invalid_attrs.present? - assert_invalid_attrs_have_errors(record, *invalid_attrs) - assert_other_attrs_have_no_errors(record, *invalid_attrs) - end + assert_not record.valid?, msg + + return if invalid_attrs.blank? + + assert_invalid_attrs_have_errors(record, *invalid_attrs) + assert_other_attrs_have_no_errors(record, *invalid_attrs) end def assert_error_message(record, attr, message) msg = message do "Expected #{mu_pp(record)} to have error message on attribute #{attr}." end + assert record.errors.messages[attr.to_sym].any? { |m| message =~ m }, msg end @@ -71,13 +77,14 @@ def assert_change(expression, message = nil, &block) expressions.zip(exps).each_with_index do |(code, e), _i| error = "#{code.inspect} didn't change" error = "#{message}.\n#{error}" if message - refute_equal(before, e.call, error) + + assert_not_equal(before, e.call, error) end end - def assert_arrays_match(expected, actual) - transform = ->(array) do - block_given? ? array.map { |element| yield(element) }.sort : array.sort + def assert_arrays_match(expected, actual, &block) + transform = lambda do |array| + block ? array.map(&block).sort : array.sort end assert_equal(transform[expected], transform[actual]) @@ -86,7 +93,7 @@ def assert_arrays_match(expected, actual) # The method used to by Test::Unit to format arguments. # Prints ActiveRecord objects in a simpler format. def mu_pp(obj) - if obj.is_a?(ActiveRecord::Base) #:nodoc: + if obj.is_a?(ActiveRecord::Base) # :nodoc: obj.to_s else super @@ -100,17 +107,21 @@ def assert_invalid_attrs_have_errors(record, *invalid_attrs) msg = message do "Expected attribute #{mu_pp(a)} to be invalid, but is valid." end - assert record.errors[a].present?, msg + + assert_predicate record.errors[a], :present?, msg end end def assert_other_attrs_have_no_errors(record, *invalid_attrs) - record.errors.each do |a, error| + record.errors.each do |error| + error_attr = error.attribute + error_msg = error.message msg = message do - "Attribute #{mu_pp(a)} not declared as invalid attribute, " \ - "but has the following error(s):\n#{mu_pp(error)}" + "Attribute #{mu_pp(error_attr)} not declared as invalid attribute, " \ + "but has the following error(s):\n#{mu_pp(error_msg)}" end - assert invalid_attrs.include?(a), msg + + assert_includes invalid_attrs, error_attr, msg end end end diff --git a/test/support/integration_helper.rb b/test/support/integration_helper.rb index c220b4e97..8012aa8e9 100644 --- a/test/support/integration_helper.rb +++ b/test/support/integration_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -8,26 +10,33 @@ module IntegrationHelper def login_as(user) employee = user.is_a?(Employee) ? user : employees(user) - # employee.update_passwd!('foobar') super(employee) end + def set_period(start_date: '1.1.2006', end_date: '31.12.2006', back_url: current_url) + visit periods_path(back_url:) + fill_in 'period_start_date', with: start_date, fill_options: { clear: :backspace } + fill_in 'period_end_date', with: end_date, fill_options: { clear: :backspace } + find('input[name=commit]').click + end + # catch some errors occuring now and then in capybara tests def timeout_safe yield rescue Errno::ECONNREFUSED, Timeout::Error, Capybara::FrozenInTime, - Capybara::ElementNotFound, - Selenium::WebDriver::Error::StaleElementReferenceError => e + Capybara::ElementNotFound => e + raise unless ENV['CI'] == true + skip e.message || e.class.name end def open_selectize(id, options = {}) element = find("##{id} + .selectize-control") - element.find('.selectize-input').click unless options[:no_click] + element.find('.selectize-input').trigger('click') unless options[:no_click] element.find('.selectize-input input').native.send_keys(:backspace) if options[:clear] - element.find('.selectize-input input').set(options[:term]) if options[:term].present? + element.find('.selectize-input input').native.send_keys(options[:term].chars) if options[:term].present? if options[:assert_empty] page.assert_no_selector('.selectize-dropdown-content') else @@ -37,27 +46,26 @@ def open_selectize(id, options = {}) end def selectize(id, value, options = {}) - open_selectize(id, options).find('.selectize-option,.option', text: value).click + open_selectize(id, options).find('.selectize-option,.option', text: value).trigger('click') end - def drag(from_node, *to_node) - action = page.driver.browser.action.click_and_hold(from_node.native) - to_node.each { |node| action = action.move_to(node.native) } - action.release.perform + def mouse + page.driver.browser.mouse end - def accept_confirmation(expected_message = nil) - if expected_message.present? - assert_equal expected_message, page.driver.browser.switch_to.alert.text - end - page.driver.browser.switch_to.alert.accept + def move_mouse_to(element) + x, y = element.native.node.find_position + mouse.move(x:, y:) end - def dismiss_confirmation(expected_message = nil) - if expected_message.present? - assert_equal expected_message, page.driver.browser.switch_to.alert.text + def drag(from_node, *to_nodes) + move_mouse_to(from_node) + mouse.down + + to_nodes.each do |to_node| + move_mouse_to(to_node) end - page.driver.browser.switch_to.alert.dismiss + mouse.up end Capybara.add_selector(:name) do @@ -65,15 +73,6 @@ def dismiss_confirmation(expected_message = nil) end def clear_cookies - browser = Capybara.current_session.driver.browser - if browser.respond_to?(:clear_cookies) - # Rack::MockSession - browser.clear_cookies - elsif browser.respond_to?(:manage) && browser.manage.respond_to?(:delete_all_cookies) - # Selenium::WebDriver - browser.manage.delete_all_cookies - else - raise "Don't know how to clear cookies. Weird driver?" - end + Capybara.current_session.driver.clear_cookies end end diff --git a/test/support/retry_on_flaky_tests.rb b/test/support/retry_on_flaky_tests.rb new file mode 100644 index 000000000..203d9312a --- /dev/null +++ b/test/support/retry_on_flaky_tests.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Include in `test_helper.rb` like this: +# +# class ActiveSupport::TestCase +# prepend RetryOnFlakyTests[FlakyError, AnotherFlakyError, max_tries: 3] +# end + +module RetryOnFlakyTests + def self.[](*error_classes, max_tries: 3) + Module.new do + define_method :max_tries do + tries = ENV.fetch('RAILS_FLAKY_TRIES', max_tries).to_i + + return 1 if max_tries < 1 + + tries + end + + define_method :error_classes do + error_classes + end + + def run_one_method(klass, method_name, reporter) + report_result = nil + max_tries.times do + result = Minitest.run_one_method(klass, method_name) + report_result ||= result + (report_result = result) and break if result.passed? + + break unless retryable_failure?(result) + end + reporter.record(report_result) + end + + def retryable_failure?(result) + result.failures.map do |failure| + failure.error.to_s + end.any? do |failure_msg| + error_classes.first { |error_class| failure_msg =~ error_class.name } + end + end + end + end +end diff --git a/test/support/small_invoice_test_helper.rb b/test/support/small_invoice_test_helper.rb deleted file mode 100644 index 7f1b59d1d..000000000 --- a/test/support/small_invoice_test_helper.rb +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (c) 2006-2020, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -module SmallInvoiceTestHelper - extend ActiveSupport::Concern - - included do - setup :stub_auth - end - - BASE_URL = 'https://api.smallinvoice.com/v2'.freeze - - def entity(name) - "Invoicing::SmallInvoice::Entity::#{name.to_s.singularize.classify}".constantize - end - - def stub_auth - stub_request(:post, "#{BASE_URL}/auth/access-tokens") - .to_return(status: 200, body: auth_body) - end - - def stub_get_entity(name, **kwargs) - args = kwargs.reverse_merge( - { - params: kwargs[:key] ? nil : '?limit=200&offset=0' - } - ) - stub_api_request(:get, name, **args) - end - - def stub_add_entity(name, **kwargs) - args = kwargs.reverse_merge( - { - body: JSON.generate(new_contact), - response: single_response(name) - } - ) - - stub_api_request(:post, name, args) - end - - def stub_edit_entity(name, **kwargs) - args = { - body: JSON.generate(new_contact) - }.merge(kwargs) - - stub_api_request(:put, name, args) - end - - def stub_delete_entity(name, **kwargs) - stub_api_request(:delete, name, **kwargs) - end - - def path(name, **kwargs) - key = kwargs[:key] - - if %i[people addresses].include?(name) - parent = kwargs[:parent] || default_client - return entity(name).path(parent, invoicing_key: key) if key - - entity(name).path(parent) - else - return entity(name).path(invoicing_key: key) if key - - entity(name).path - end - end - - def path_url(name, **kwargs) - path(name, **kwargs).join('/') - end - - private - - def stub_api_request(method, name, **kwargs) - key = kwargs[:key] - path = kwargs[:path] || path_url(name, **kwargs) - params = kwargs[:params] - url = kwargs[:url] || "#{BASE_URL}/#{path}#{params}" - body = kwargs[:body] - response = kwargs[:response] - response ||= key ? single_response(name) : response(name) - - stub = stub_request(method, url) - stub = stub.with(body: body) if body - stub = stub.to_return(status: 200, body: response) if response - stub - end - - def new_contact - entity(:contacts).new(default_client).to_hash - end - - def default_client - clients(:puzzle) - end - - def client_with_key - default_client.invoicing_key = 1234 - default_client - end - - def single_response(name) - response(name.to_s.singularize) - end - - def response(name) - file_fixture("small_invoice/#{name}.json").read - rescue StandardError - nil - end - - def id(name) - data = JSON.parse( - file_fixture("small_invoice/#{name}.json").read - ) - - return data['items'].first['id'] if data.key? 'items' - - data['item']['id'] - rescue StandardError - nil - end - - def auth_body - JSON.generate( - { - access_token: '1234', - expires_in: 43200, - token_type: 'Bearer' - } - ) - end -end diff --git a/test/tarantula/tarantula_test.rb_ b/test/tarantula/tarantula_test.rb_ index dd590c595..95cb4fe0d 100644 --- a/test/tarantula/tarantula_test.rb_ +++ b/test/tarantula/tarantula_test.rb_ @@ -120,7 +120,7 @@ class TarantulaTest < ActionDispatch::IntegrationTest end def set_credentials(manager) - user.update_attributes!( + user.update!( shortname: CREDENTIALS.first, passwd: Employee.encode(CREDENTIALS.last), management: manager) diff --git a/test/test_helper.rb b/test/test_helper.rb index d451246b0..77e1e5c4a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of # PuzzleTime and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at @@ -5,120 +7,133 @@ ENV['RAILS_ENV'] = 'test' -if ENV['TEST_REPORTS'] - require 'simplecov' - require 'simplecov-rcov' - SimpleCov.coverage_dir 'test/coverage' - # use this formatter for jenkins compatibility - SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter - SimpleCov.command_name 'Unit Tests' - SimpleCov.start 'rails' - - require 'minitest/reporters' - MiniTest::Reporters.use! [MiniTest::Reporters::DefaultReporter.new, - MiniTest::Reporters::JUnitReporter.new] -end - -require File.expand_path('../../config/environment', __FILE__) +# if ENV['TEST_REPORTS'] +# require 'simplecov' +# require 'simplecov-rcov' +# SimpleCov.coverage_dir 'test/coverage' +# # use this formatter for jenkins compatibility +# SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter +# SimpleCov.command_name 'Unit Tests' +# SimpleCov.start 'rails' + +# require 'minitest/reporters' +# MiniTest::Reporters.use! [MiniTest::Reporters::DefaultReporter.new, +# MiniTest::Reporters::JUnitReporter.new] +# end + +require File.expand_path('../config/environment', __dir__) Rails.env = 'test' require 'rails/test_help' require 'mocha/minitest' require 'capybara/rails' - -require 'webmock/minitest' -WebMock.disable_net_connect!( - allow_localhost: true, # required for selenium - allow: [ - 'github.com', # required for webdrivers/geckodriver - /github-production-release-asset-\w+.s3.amazonaws.com/, # required for webdrivers/geckodriver - /github-releases.githubusercontent.com/, # required for webdrivers/geckodriver - /objects.githubusercontent.com/, # required for webdrivers/geckodriver - 'chromedriver.storage.googleapis.com' # required for webdrivers/chromedriver - ] -) - Settings.reload! -Dir[Rails.root.join('test/support/**/*.rb')].sort.each { |f| require f } - -Capybara.register_driver :selenium do |app| - require 'selenium/webdriver' - - Selenium::WebDriver::Firefox::Binary.path = ENV['FIREFOX_PATH'] if ENV['FIREFOX_PATH'] - capa = Selenium::WebDriver::Remote::Capabilities.firefox(marionette: true) - Capybara::Selenium::Driver.new(app, browser: :firefox, desired_capabilities: capa) +Dir[Rails.root.join('test/support/**/*.rb')].each { |f| require f } + +# load Cuprite Capybara integration +require 'capybara/cuprite' + +Capybara.register_driver :chrome do |app| + Capybara::Cuprite::Driver.new( + app, + window_size: [1920, 1080], + # See additional options for Dockerized environment in the respective section of this article + browser_options: { + # Required for ARM chips on which CI might run + 'disable-smooth-scrolling' => true + }, + # Increase Chrome startup wait time (required for stable CI builds) + process_timeout: 10, + # Enable debugging capabilities + inspector: true, + # Allow running Chrome in a headful mode by setting HEADLESS env + # var to a falsey value + headless: !ENV['HEADLESS'].in?(%w[n 0 no false]) + ) end -Capybara.server = :puma, { Silent: true } -Capybara.server_port = ENV['CAPYBARA_SERVER_PORT'].to_i if ENV['CAPYBARA_SERVER_PORT'] -Capybara.default_driver = :selenium +Capybara.default_driver = Capybara.javascript_driver = :chrome +Capybara.server = :puma, { Silent: true } # Silence that nasty log output Capybara.default_max_wait_time = 5 -unless ENV['HEADLESS'] == 'false' - require 'headless' - - headless = Headless.new(destroy_at_exit: false) - headless.start -end - -class ActiveSupport::TestCase - include CustomAssertions - - # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. - # - # Note: You'll currently still have to declare fixtures explicitly in integration tests - # -- they do not yet inherit this setting - fixtures :all - - # Add more helper methods to be used by all tests here... - - def login - login_as(:mark) - end +module ActiveSupport + class TestCase + include CustomAssertions + + extend RetryOnFlakyTests[ + # randomly happening on CI + Ferrum::PendingConnectionsError, + # race condition when trying to move mouse to element, can happen e.g. after fade-in/out of modal dialog + Ferrum::CoordinatesNotFoundError, + # race condition when trying to click element, can happen e.g. after fade-in/out of modal dialog + Capybara::Cuprite::MouseEventFailed, + max_tries: 3 + ] + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + # + # Note: You'll currently still have to declare fixtures explicitly in integration tests + # -- they do not yet inherit this setting + fixtures :all + + # Add more helper methods to be used by all tests here... + + def login + login_as(:mark) + end - def login_as(user) - employee = employees(user) - sign_in employee - @request.session[:user_id] = user ? employee.id : nil - @request.session[:employee_id] = user ? employee.id : nil - end + def login_as(user) + employee = employees(user) + sign_in employee + @request.session[:user_id] = user ? employee.id : nil + @request.session[:employee_id] = user ? employee.id : nil + end - def logout - sign_out Employee - @request.session[:user_id] = @request.session[:employee_id] = nil - end + def logout + sign_out Employee + @request.session[:user_id] = @request.session[:employee_id] = nil + end - # Since we've removed the hardcoded regular holidays, insert them manually - def setup_regular_holidays(years) - years = [years].flatten.compact - dates = [[1, 1], [2, 1], [1, 8], [25, 12], [26, 12]] - dates.each do |day, month| - years.each do |year| - Holiday.create!(holiday_date: Date.new(year, month, day), musthours_day: 0) + # Since we've removed the hardcoded regular holidays, insert them manually + def setup_regular_holidays(years) + years = [years].flatten.compact + dates = [[1, 1], [2, 1], [1, 8], [25, 12], [26, 12]] + dates.each do |day, month| + years.each do |year| + Holiday.create!(holiday_date: Date.new(year, month, day), musthours_day: 0) + end end end + + def set_period(start_date: '1.1.2006', end_date: '31.12.2006') + @controller.session[:period] = [start_date, end_date] + end end end -class ActionDispatch::IntegrationTest - include Capybara::DSL - include Devise::Test::IntegrationHelpers - include IntegrationHelper +module ActionDispatch + class IntegrationTest + include Capybara::DSL + include Devise::Test::IntegrationHelpers + include IntegrationHelper - DatabaseCleaner.strategy = :truncation + DatabaseCleaner.strategy = :truncation - self.use_transactional_tests = false + self.use_transactional_tests = false - setup do - clear_cookies - DatabaseCleaner.start - end + setup do + clear_cookies + DatabaseCleaner.start + end - teardown do - DatabaseCleaner.clean + teardown do + DatabaseCleaner.clean + end end end -class ActionController::TestCase - include Devise::Test::ControllerHelpers +module ActionController + class TestCase + include Devise::Test::ControllerHelpers + end end diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/assets/javascripts/chartjs-plugin-annotation.min.js b/vendor/assets/javascripts/chartjs-plugin-annotation.min.js index ec326acf5..4f016d503 100644 --- a/vendor/assets/javascripts/chartjs-plugin-annotation.min.js +++ b/vendor/assets/javascripts/chartjs-plugin-annotation.min.js @@ -1,10 +1,7 @@ /*! - * chartjs-plugin-annotation.js - * http://chartjs.org/ - * Version: 0.5.7 - * - * Copyright 2016 Evert Timberg - * Released under the MIT license - * https://github.com/chartjs/Chart.Annotation.js/blob/master/LICENSE.md +* chartjs-plugin-annotation v2.2.1 +* https://www.chartjs.org/chartjs-plugin-annotation/index + * (c) 2023 chartjs-plugin-annotation Contributors + * Released under the MIT License */ -!function e(t,n,i){function o(r,l){if(!n[r]){if(!t[r]){var s="function"==typeof require&&require;if(!l&&s)return s(r,!0);if(a)return a(r,!0);var c=new Error("Cannot find module '"+r+"'");throw c.code="MODULE_NOT_FOUND",c}var u=n[r]={exports:{}};t[r][0].call(u.exports,function(e){var n=t[r][1][e];return o(n?n:e)},u,u.exports,e,t,n,i)}return n[r].exports}for(var a="function"==typeof require&&require,r=0;r<i.length;r++)o(i[r]);return o}({1:[function(e,t,n){},{}],2:[function(e,t,n){t.exports=function(t){function n(e){a.decorate(e,"afterDataLimits",function(e,t){e&&e(t),a.adjustScaleRange(t)})}function i(e){return function(t,n){var i=t.annotation.options.drawTime;a.elements(t).filter(function(t){return e===(t.options.drawTime||i)}).forEach(function(e){e.transition(n).draw()})}}var o=t.helpers,a=e("./helpers.js")(t),r=e("./events.js")(t),l=t.Annotation.types;return{beforeInit:function(e){var t=e.options,i=e.annotation={elements:{},options:a.initConfig(t.annotation||{}),onDestroy:[],firstRun:!0,supported:!1};e.ensureScalesHaveIDs(),t.scales&&(i.supported=!0,o.each(t.scales.xAxes,n),o.each(t.scales.yAxes,n))},beforeUpdate:function(e){var t=e.annotation;if(t.supported){t.firstRun?t.firstRun=!1:t.options=a.initConfig(e.options.annotation||{});var n=[];t.options.annotations.forEach(function(i){var o=i.id||a.objectId();if(!t.elements[o]&&l[i.type]){var r=l[i.type],s=new r({id:o,options:i,chartInstance:e});s.initialize(),t.elements[o]=s,i.id=o,n.push(o)}else t.elements[o]&&n.push(o)}),Object.keys(t.elements).forEach(function(e){n.indexOf(e)===-1&&(t.elements[e].destroy(),delete t.elements[e])})}},afterScaleUpdate:function(e){a.elements(e).forEach(function(e){e.configure()})},beforeDatasetsDraw:i("beforeDatasetsDraw"),afterDatasetsDraw:i("afterDatasetsDraw"),afterDraw:i("afterDraw"),afterInit:function(e){var t=e.annotation.options.events;if(o.isArray(t)&&t.length>0){var n=e.chart.canvas,i=r.dispatcher.bind(e);r.collapseHoverEvents(t).forEach(function(t){o.addEvent(n,t,i),e.annotation.onDestroy.push(function(){o.removeEvent(n,t,i)})})}},destroy:function(e){for(var t=e.annotation.onDestroy;t.length>0;)t.pop()()}}}},{"./events.js":4,"./helpers.js":5}],3:[function(e,t,n){t.exports=function(e){var t=e.helpers,n=e.Element.extend({initialize:function(){this.hidden=!1,this.hovering=!1,this._model=t.clone(this._model)||{},this.setDataLimits()},destroy:function(){},setDataLimits:function(){},configure:function(){},inRange:function(){},getCenterPoint:function(){},getWidth:function(){},getHeight:function(){},getArea:function(){},draw:function(){}});return n}},{}],4:[function(e,t,n){t.exports=function(t){function n(e){var t=!1,n=e.filter(function(e){switch(e){case"mouseenter":case"mouseover":case"mouseout":case"mouseleave":return t=!0,!1;default:return!0}});return t&&n.indexOf("mousemove")===-1&&n.push("mousemove"),n}function i(e){var t=this.annotation,i=a.elements(this),r=o.getRelativePosition(e,this.chart),l=a.getNearestItems(i,r),s=n(t.options.events),c=t.options.dblClickSpeed,u=[],f=a.getEventHandlerName(e.type),d=(l||{}).options;if("mousemove"===e.type&&(l&&!l.hovering?["mouseenter","mouseover"].forEach(function(t){var n=a.getEventHandlerName(t),i=a.createMouseEvent(t,e);l.hovering=!0,"function"==typeof d[n]&&u.push([d[n],i,l])}):l||i.forEach(function(t){if(t.hovering){t.hovering=!1;var n=t.options;["mouseout","mouseleave"].forEach(function(i){var o=a.getEventHandlerName(i),r=a.createMouseEvent(i,e);"function"==typeof n[o]&&u.push([n[o],r,t])})}})),l&&s.indexOf("dblclick")>-1&&"function"==typeof d.onDblclick){if("click"===e.type&&"function"==typeof d.onClick)return clearTimeout(l.clickTimeout),l.clickTimeout=setTimeout(function(){delete l.clickTimeout,d.onClick.call(l,e)},c),e.stopImmediatePropagation(),void e.preventDefault();"dblclick"===e.type&&l.clickTimeout&&(clearTimeout(l.clickTimeout),delete l.clickTimeout)}l&&"function"==typeof d[f]&&0===u.length&&u.push([d[f],e,l]),u.length>0&&(e.stopImmediatePropagation(),e.preventDefault(),u.forEach(function(e){e[0].call(e[2],e[1])}))}var o=t.helpers,a=e("./helpers.js")(t);return{dispatcher:i,collapseHoverEvents:n}}},{"./helpers.js":5}],5:[function(e,t,n){function i(){}function o(e){var t=e.annotation.elements;return Object.keys(t).map(function(e){return t[e]})}function a(){return Math.random().toString(36).substr(2,6)}function r(e){return null!==e&&"undefined"!=typeof e&&("number"==typeof e?isFinite(e):!!e)}function l(e,t,n){var i="$";e[i+t]||(e[t]?(e[i+t]=e[t].bind(e),e[t]=function(){var o=[e[i+t]].concat(Array.prototype.slice.call(arguments));return n.apply(e,o)}):e[t]=function(){var t=[void 0].concat(Array.prototype.slice.call(arguments));return n.apply(e,t)})}function s(e,t){e.forEach(function(e){(t?e[t]:e)()})}function c(e){return"on"+e[0].toUpperCase()+e.substring(1)}function u(e,t){try{return new MouseEvent(e,t)}catch(n){try{var i=document.createEvent("MouseEvent");return i.initMouseEvent(e,t.canBubble,t.cancelable,t.view,t.detail,t.screenX,t.screenY,t.clientX,t.clientY,t.ctrlKey,t.altKey,t.shiftKey,t.metaKey,t.button,t.relatedTarget),i}catch(o){var a=document.createEvent("Event");return a.initEvent(e,t.canBubble,t.cancelable),a}}}t.exports=function(e){function t(t){return t=h.configMerge(e.Annotation.defaults,t),h.isArray(t.annotations)&&t.annotations.forEach(function(t){t.label=h.configMerge(e.Annotation.labelDefaults,t.label)}),t}function n(e,t,n,i){var o=t.filter(function(t){return!!t._model.ranges[e]}).map(function(t){return t._model.ranges[e]}),a=o.map(function(e){return Number(e.min)}).reduce(function(e,t){return isFinite(t)&&!isNaN(t)&&t<e?t:e},n),r=o.map(function(e){return Number(e.max)}).reduce(function(e,t){return isFinite(t)&&!isNaN(t)&&t>e?t:e},i);return{min:a,max:r}}function f(e){var t=n(e.id,o(e.chart),e.min,e.max);"undefined"==typeof e.options.ticks.min&&"undefined"==typeof e.options.ticks.suggestedMin&&(e.min=t.min),"undefined"==typeof e.options.ticks.max&&"undefined"==typeof e.options.ticks.suggestedMax&&(e.max=t.max),e.handleTickRangeOptions&&e.handleTickRangeOptions()}function d(e,t){var n=Number.POSITIVE_INFINITY;return e.filter(function(e){return e.inRange(t.x,t.y)}).reduce(function(e,i){var o=i.getCenterPoint(),a=h.distanceBetweenPoints(t,o);return a<n?(e=[i],n=a):a===n&&e.push(i),e},[]).sort(function(e,t){var n=e.getArea(),i=t.getArea();return n>i||n<i?n-i:e._index-t._index}).slice(0,1)[0]}var h=e.helpers;return{initConfig:t,elements:o,callEach:s,noop:i,objectId:a,isValid:r,decorate:l,adjustScaleRange:f,getNearestItems:d,getEventHandlerName:c,createMouseEvent:u}}},{}],6:[function(e,t,n){var i=e("chart.js");i="function"==typeof i?i:window.Chart,i.Annotation=i.Annotation||{},i.Annotation.drawTimeOptions={afterDraw:"afterDraw",afterDatasetsDraw:"afterDatasetsDraw",beforeDatasetsDraw:"beforeDatasetsDraw"},i.Annotation.defaults={drawTime:"afterDatasetsDraw",dblClickSpeed:350,events:[],annotations:[]},i.Annotation.labelDefaults={backgroundColor:"rgba(0,0,0,0.8)",fontFamily:i.defaults.global.defaultFontFamily,fontSize:i.defaults.global.defaultFontSize,fontStyle:"bold",fontColor:"#fff",xPadding:6,yPadding:6,cornerRadius:6,position:"center",xAdjust:0,yAdjust:0,enabled:!1,content:null},i.Annotation.Element=e("./element.js")(i),i.Annotation.types={line:e("./types/line.js")(i),box:e("./types/box.js")(i)};var o=e("./annotation.js")(i);t.exports=o,i.pluginService.register(o)},{"./annotation.js":2,"./element.js":3,"./types/box.js":7,"./types/line.js":8,"chart.js":1}],7:[function(e,t,n){t.exports=function(t){var n=e("../helpers.js")(t),i=t.Annotation.Element.extend({setDataLimits:function(){var e=this._model,t=this.options,i=this.chartInstance,o=i.scales[t.xScaleID],a=i.scales[t.yScaleID],r=i.chartArea;if(e.ranges={},r){var l=0,s=0;o&&(l=n.isValid(t.xMin)?t.xMin:o.getPixelForValue(r.left),s=n.isValid(t.xMax)?t.xMax:o.getPixelForValue(r.right),e.ranges[t.xScaleID]={min:Math.min(l,s),max:Math.max(l,s)}),a&&(l=n.isValid(t.yMin)?t.yMin:a.getPixelForValue(r.bottom),s=n.isValid(t.yMax)?t.yMax:a.getPixelForValue(r.top),e.ranges[t.yScaleID]={min:Math.min(l,s),max:Math.max(l,s)})}},configure:function(){var e=this._model,t=this.options,i=this.chartInstance,o=i.scales[t.xScaleID],a=i.scales[t.yScaleID],r=i.chartArea;e.clip={x1:r.left,x2:r.right,y1:r.top,y2:r.bottom};var l,s,c=r.left,u=r.top,f=r.right,d=r.bottom;o&&(l=n.isValid(t.xMin)?o.getPixelForValue(t.xMin):r.left,s=n.isValid(t.xMax)?o.getPixelForValue(t.xMax):r.right,c=Math.min(l,s),f=Math.max(l,s)),a&&(l=n.isValid(t.yMin)?a.getPixelForValue(t.yMin):r.bottom,s=n.isValid(t.yMax)?a.getPixelForValue(t.yMax):r.top,u=Math.min(l,s),d=Math.max(l,s)),e.left=c,e.top=u,e.right=f,e.bottom=d,e.borderColor=t.borderColor,e.borderWidth=t.borderWidth,e.backgroundColor=t.backgroundColor},inRange:function(e,t){var n=this._model;return n&&e>=n.left&&e<=n.right&&t>=n.top&&t<=n.bottom},getCenterPoint:function(){var e=this._model;return{x:(e.right+e.left)/2,y:(e.bottom+e.top)/2}},getWidth:function(){var e=this._model;return Math.abs(e.right-e.left)},getHeight:function(){var e=this._model;return Math.abs(e.bottom-e.top)},getArea:function(){return this.getWidth()*this.getHeight()},draw:function(){var e=this._view,t=this.chartInstance.chart.ctx;t.save(),t.beginPath(),t.rect(e.clip.x1,e.clip.y1,e.clip.x2-e.clip.x1,e.clip.y2-e.clip.y1),t.clip(),t.lineWidth=e.borderWidth,t.strokeStyle=e.borderColor,t.fillStyle=e.backgroundColor;var n=e.right-e.left,i=e.bottom-e.top;t.fillRect(e.left,e.top,n,i),t.strokeRect(e.left,e.top,n,i),t.restore()}});return i}},{"../helpers.js":5}],8:[function(e,t,n){t.exports=function(t){function n(e){var t=(e.x2-e.x1)/(e.y2-e.y1),n=e.x1||0;this.m=t,this.b=n,this.getX=function(i){return t*(i-e.y1)+n},this.getY=function(i){return(i-n)/t+e.y1},this.intersects=function(e,t,n){n=n||.001;var i=this.getY(e),o=this.getX(t);return(!isFinite(i)||Math.abs(t-i)<n)&&(!isFinite(o)||Math.abs(e-o)<n)}}function i(e,t,n,i,o){var a=e.line,s={},c=0,u=0;switch(!0){case e.mode==l&&"top"==e.labelPosition:u=o+e.labelYAdjust,c=t/2+e.labelXAdjust,s.y=e.y1+u,s.x=(isFinite(a.m)?a.getX(s.y):e.x1)-c;break;case e.mode==l&&"bottom"==e.labelPosition:u=n+o+e.labelYAdjust,c=t/2+e.labelXAdjust,s.y=e.y2-u,s.x=(isFinite(a.m)?a.getX(s.y):e.x1)-c;break;case e.mode==r&&"left"==e.labelPosition:c=i+e.labelXAdjust,u=-(n/2)+e.labelYAdjust,s.x=e.x1+c,s.y=a.getY(s.x)+u;break;case e.mode==r&&"right"==e.labelPosition:c=t+i+e.labelXAdjust,u=-(n/2)+e.labelYAdjust,s.x=e.x2-c,s.y=a.getY(s.x)+u;break;default:s.x=(e.x1+e.x2-t)/2+e.labelXAdjust,s.y=(e.y1+e.y2-n)/2+e.labelYAdjust}return s}var o=t.helpers,a=e("../helpers.js")(t),r="horizontal",l="vertical",s=t.Annotation.Element.extend({setDataLimits:function(){var e=this._model,t=this.options;e.ranges={},e.ranges[t.scaleID]={min:t.value,max:t.endValue||t.value}},configure:function(){var e,t,l=this._model,s=this.options,c=this.chartInstance,u=c.chart.ctx,f=c.scales[s.scaleID];if(f&&(e=a.isValid(s.value)?f.getPixelForValue(s.value):NaN,t=a.isValid(s.endValue)?f.getPixelForValue(s.endValue):e),!isNaN(e)){var d=c.chartArea;l.clip={x1:d.left,x2:d.right,y1:d.top,y2:d.bottom},this.options.mode==r?(l.x1=d.left,l.x2=d.right,l.y1=e,l.y2=t):(l.y1=d.top,l.y2=d.bottom,l.x1=e,l.x2=t),l.line=new n(l),l.mode=s.mode,l.labelBackgroundColor=s.label.backgroundColor,l.labelFontFamily=s.label.fontFamily,l.labelFontSize=s.label.fontSize,l.labelFontStyle=s.label.fontStyle,l.labelFontColor=s.label.fontColor,l.labelXPadding=s.label.xPadding,l.labelYPadding=s.label.yPadding,l.labelCornerRadius=s.label.cornerRadius,l.labelPosition=s.label.position,l.labelXAdjust=s.label.xAdjust,l.labelYAdjust=s.label.yAdjust,l.labelEnabled=s.label.enabled,l.labelContent=s.label.content,u.font=o.fontString(l.labelFontSize,l.labelFontStyle,l.labelFontFamily);var h=u.measureText(l.labelContent).width,b=u.measureText("M").width,p=i(l,h,b,l.labelXPadding,l.labelYPadding);l.labelX=p.x-l.labelXPadding,l.labelY=p.y-l.labelYPadding,l.labelWidth=h+2*l.labelXPadding,l.labelHeight=b+2*l.labelYPadding,l.borderColor=s.borderColor,l.borderWidth=s.borderWidth,l.borderDash=s.borderDash||[],l.borderDashOffset=s.borderDashOffset||0}},inRange:function(e,t){var n=this._model;return n.line&&n.line.intersects(e,t,this.getHeight())||n.labelEnabled&&n.labelContent&&e>=n.labelX&&e<=n.labelX+n.labelWidth&&t>=n.labelY&&t<=n.labelY+n.labelHeight},getCenterPoint:function(){return{x:(this._model.x2+this._model.x1)/2,y:(this._model.y2+this._model.y1)/2}},getWidth:function(){return Math.abs(this._model.right-this._model.left)},getHeight:function(){return this._model.borderWidth||1},getArea:function(){return Math.sqrt(Math.pow(this.getWidth(),2)+Math.pow(this.getHeight(),2))},draw:function(){var e=this._view,t=this.chartInstance.chart.ctx;e.clip&&(t.save(),t.beginPath(),t.rect(e.clip.x1,e.clip.y1,e.clip.x2-e.clip.x1,e.clip.y2-e.clip.y1),t.clip(),t.lineWidth=e.borderWidth,t.strokeStyle=e.borderColor,t.setLineDash&&t.setLineDash(e.borderDash),t.lineDashOffset=e.borderDashOffset,t.beginPath(),t.moveTo(e.x1,e.y1),t.lineTo(e.x2,e.y2),t.stroke(),e.labelEnabled&&e.labelContent&&(t.beginPath(),t.rect(e.clip.x1,e.clip.y1,e.clip.x2-e.clip.x1,e.clip.y2-e.clip.y1),t.clip(),t.fillStyle=e.labelBackgroundColor,o.drawRoundedRectangle(t,e.labelX,e.labelY,e.labelWidth,e.labelHeight,e.labelCornerRadius),t.fill(),t.font=o.fontString(e.labelFontSize,e.labelFontStyle,e.labelFontFamily),t.fillStyle=e.labelFontColor,t.textAlign="center",t.textBaseline="middle",t.fillText(e.labelContent,e.labelX+e.labelWidth/2,e.labelY+e.labelHeight/2)),t.restore())}});return s}},{"../helpers.js":5}]},{},[6]); \ No newline at end of file +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("chart.js"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["chart.js","chart.js/helpers"],e):(t="undefined"!=typeof globalThis?globalThis:t||self)["chartjs-plugin-annotation"]=e(t.Chart,t.Chart.helpers)}(this,(function(t,e){"use strict";const o={modes:{point:(t,e)=>r(t,e,{intersect:!0}),nearest:(t,o,n)=>function(t,o,n){let i=Number.POSITIVE_INFINITY;return r(t,o,n).reduce(((t,r)=>{const s=r.getCenterPoint(),a=function(t,e,o){if("x"===o)return{x:t.x,y:e.y};if("y"===o)return{x:e.x,y:t.y};return e}(o,s,n.axis),d=e.distanceBetweenPoints(o,a);return d<i?(t=[r],i=d):d===i&&t.push(r),t}),[]).sort(((t,e)=>t._index-e._index)).slice(0,1)}(t,o,n),x:(t,e,o)=>r(t,e,{intersect:o.intersect,axis:"x"}),y:(t,e,o)=>r(t,e,{intersect:o.intersect,axis:"y"})}};function n(t,e,n){return(o.modes[n.mode]||o.modes.nearest)(t,e,n)}function r(t,e,o){return t.visibleElements.filter((t=>o.intersect?t.inRange(e.x,e.y):function(t,e,o){return"x"!==o&&"y"!==o?t.inRange(e.x,e.y,"x",!0)||t.inRange(e.x,e.y,"y",!0):t.inRange(e.x,e.y,o,!0)}(t,e,o.axis)))}const i=(t,e)=>e>t||t.length>e.length&&t.slice(0,e.length)===e,s=.001,a=(t,e,o)=>Math.min(o,Math.max(e,t));function d(t,e,o){for(const n of Object.keys(t))t[n]=a(t[n],e,o);return t}function l(t,{x:e,y:o,x2:n,y2:r},i,a){const d=a/2,l=t.x>=e-d-s&&t.x<=n+d+s,c=t.y>=o-d-s&&t.y<=r+d+s;return"x"===i?l:("y"===i||l)&&c}function c(t,e){const{centerX:o,centerY:n}=t.getProps(["centerX","centerY"],e);return{x:o,y:n}}const h=t=>"string"==typeof t&&t.endsWith("%"),u=t=>parseFloat(t)/100,f=t=>a(u(t),0,1);function x(t,e){return"start"===e?0:"end"===e?t:h(e)?f(e)*t:t/2}function y(t,e,o=!0){return"number"==typeof e?e:h(e)?(o?f(e):u(e))*t:t}function p(t,o="center"){return e.isObject(t)?{x:e.valueOrDefault(t.x,o),y:e.valueOrDefault(t.y,o)}:{x:t=e.valueOrDefault(t,o),y:t}}function b(t){return t&&(e.defined(t.xValue)||e.defined(t.yValue))}function g(t,o,n,r=!1){const i=n.init;if(i)return!0===i?v(o,r):function(t,o,n){if(!0===n)return v(t,o);if(e.isObject(n))return n}(o,r,e.callback(i,[{chart:t,properties:o,options:n}]))}function m(t,o,n){let r=!1;return o.forEach((o=>{e.isFunction(t[o])?(r=!0,n[o]=t[o]):e.defined(n[o])&&delete n[o]})),r}function v({centerX:t,centerY:e},o){return o?{centerX:t,centerY:e,radius:0,width:0,height:0}:{x:t,y:e,x2:t,y2:e,width:0,height:0}}const w=new Map,M=t=>isNaN(t)||t<=0,P=t=>t.reduce((function(t,e){return t+=e.string}),"");function S(t){if(t&&"object"==typeof t){const e=t.toString();return"[object HTMLImageElement]"===e||"[object HTMLCanvasElement]"===e}}function C(t,{x:o,y:n},r){r&&(t.translate(o,n),t.rotate(e.toRadians(r)),t.translate(-o,-n))}function k(t,e){if(e&&e.borderWidth)return t.lineCap=e.borderCapStyle,t.setLineDash(e.borderDash),t.lineDashOffset=e.borderDashOffset,t.lineJoin=e.borderJoinStyle,t.lineWidth=e.borderWidth,t.strokeStyle=e.borderColor,!0}function D(t,e){t.shadowColor=e.backgroundShadowColor,t.shadowBlur=e.shadowBlur,t.shadowOffsetX=e.shadowOffsetX,t.shadowOffsetY=e.shadowOffsetY}function A(t,o){const n=o.content;if(S(n))return{width:y(n.width,o.width),height:y(n.height,o.height)};const r=o.font,i=e.isArray(r)?r.map((t=>e.toFont(t))):[e.toFont(r)],s=o.textStrokeWidth,a=e.isArray(n)?n:[n],d=a.join()+P(i)+s+(t._measureText?"-spriting":"");return w.has(d)||w.set(d,function(t,e,o,n){t.save();const r=e.length;let i=0,s=n;for(let a=0;a<r;a++){const r=o[Math.min(a,o.length-1)];t.font=r.string;const d=e[a];i=Math.max(i,t.measureText(d).width+n),s+=r.lineHeight}return t.restore(),{width:i,height:s}}(t,a,i,s)),w.get(d)}function j(t,o,n){const{x:r,y:i,width:s,height:a}=o;t.save(),D(t,n);const l=k(t,n);t.fillStyle=n.backgroundColor,t.beginPath(),e.addRoundedRectPath(t,{x:r,y:i,w:s,h:a,radius:d(e.toTRBLCorners(n.borderRadius),0,Math.min(s,a)/2)}),t.closePath(),t.fill(),l&&(t.shadowColor=n.borderShadowColor,t.stroke()),t.restore()}function I(t,o,n){const r=n.content;if(S(r))return t.save(),t.globalAlpha=function(t,o){const n=e.isNumber(t)?t:o;return e.isNumber(n)?a(n,0,1):1}(n.opacity,r.style.opacity),t.drawImage(r,o.x,o.y,o.width,o.height),void t.restore();const i=e.isArray(r)?r:[r],s=n.font,d=e.isArray(s)?s.map((t=>e.toFont(t))):[e.toFont(s)],l=n.color,c=e.isArray(l)?l:[l],h=function(t,e){const{x:o,width:n}=t,r=e.textAlign;return"center"===r?o+n/2:"end"===r||"right"===r?o+n:o}(o,n),u=o.y+n.textStrokeWidth/2;t.save(),t.textBaseline="middle",t.textAlign=n.textAlign,function(t,e){if(e.textStrokeWidth>0)return t.lineJoin="round",t.miterLimit=2,t.lineWidth=e.textStrokeWidth,t.strokeStyle=e.textStrokeColor,!0}(t,n)&&function(t,{x:e,y:o},n,r){t.beginPath();let i=0;n.forEach((function(n,s){const a=r[Math.min(s,r.length-1)],d=a.lineHeight;t.font=a.string,t.strokeText(n,e,o+d/2+i),i+=d})),t.stroke()}(t,{x:h,y:u},i,d),function(t,{x:e,y:o},n,{fonts:r,colors:i}){let s=0;n.forEach((function(n,a){const d=i[Math.min(a,i.length-1)],l=r[Math.min(a,r.length-1)],c=l.lineHeight;t.beginPath(),t.font=l.string,t.fillStyle=d,t.fillText(n,e,o+c/2+s),s+=c,t.fill()}))}(t,{x:h,y:u},i,{fonts:d,colors:c}),t.restore()}function O(t,o,n,r){const{radius:i,options:s}=o,a=s.pointStyle,d=s.rotation;let l=(d||0)*e.RAD_PER_DEG;if(S(a))return t.save(),t.translate(n,r),t.rotate(l),t.drawImage(a,-a.width/2,-a.height/2,a.width,a.height),void t.restore();M(i)||function(t,{x:o,y:n,radius:r,rotation:i,style:s,rad:a}){let d,l,c,h;switch(t.beginPath(),s){default:t.arc(o,n,r,0,e.TAU),t.closePath();break;case"triangle":t.moveTo(o+Math.sin(a)*r,n-Math.cos(a)*r),a+=e.TWO_THIRDS_PI,t.lineTo(o+Math.sin(a)*r,n-Math.cos(a)*r),a+=e.TWO_THIRDS_PI,t.lineTo(o+Math.sin(a)*r,n-Math.cos(a)*r),t.closePath();break;case"rectRounded":h=.516*r,c=r-h,d=Math.cos(a+e.QUARTER_PI)*c,l=Math.sin(a+e.QUARTER_PI)*c,t.arc(o-d,n-l,h,a-e.PI,a-e.HALF_PI),t.arc(o+l,n-d,h,a-e.HALF_PI,a),t.arc(o+d,n+l,h,a,a+e.HALF_PI),t.arc(o-l,n+d,h,a+e.HALF_PI,a+e.PI),t.closePath();break;case"rect":if(!i){c=Math.SQRT1_2*r,t.rect(o-c,n-c,2*c,2*c);break}a+=e.QUARTER_PI;case"rectRot":d=Math.cos(a)*r,l=Math.sin(a)*r,t.moveTo(o-d,n-l),t.lineTo(o+l,n-d),t.lineTo(o+d,n+l),t.lineTo(o-l,n+d),t.closePath();break;case"crossRot":a+=e.QUARTER_PI;case"cross":d=Math.cos(a)*r,l=Math.sin(a)*r,t.moveTo(o-d,n-l),t.lineTo(o+d,n+l),t.moveTo(o+l,n-d),t.lineTo(o-l,n+d);break;case"star":d=Math.cos(a)*r,l=Math.sin(a)*r,t.moveTo(o-d,n-l),t.lineTo(o+d,n+l),t.moveTo(o+l,n-d),t.lineTo(o-l,n+d),a+=e.QUARTER_PI,d=Math.cos(a)*r,l=Math.sin(a)*r,t.moveTo(o-d,n-l),t.lineTo(o+d,n+l),t.moveTo(o+l,n-d),t.lineTo(o-l,n+d);break;case"line":d=Math.cos(a)*r,l=Math.sin(a)*r,t.moveTo(o-d,n-l),t.lineTo(o+d,n+l);break;case"dash":t.moveTo(o,n),t.lineTo(o+Math.cos(a)*r,n+Math.sin(a)*r)}t.fill()}(t,{x:n,y:r,radius:i,rotation:d,style:a,rad:l})}const T={xScaleID:{min:"xMin",max:"xMax",start:"left",end:"right",startProp:"x",endProp:"x2"},yScaleID:{min:"yMin",max:"yMax",start:"bottom",end:"top",startProp:"y",endProp:"y2"}};function R(t,o,n){return o="number"==typeof o?o:t.parse(o),e.isFinite(o)?t.getPixelForValue(o):n}function Y(t,e,o){const n=e[o];if(n||"scaleID"===o)return n;const r=o.charAt(0),i=Object.values(t).filter((t=>t.axis&&t.axis===r));return i.length?i[0].id:r}function X(t,e){if(t){const o=t.options.reverse;return{start:R(t,e.min,o?e.end:e.start),end:R(t,e.max,o?e.start:e.end)}}}function E(t,e){const{chartArea:o,scales:n}=t,r=n[Y(n,e,"xScaleID")],i=n[Y(n,e,"yScaleID")];let s=o.width/2,a=o.height/2;return r&&(s=R(r,e.xValue,r.left+r.width/2)),i&&(a=R(i,e.yValue,i.top+i.height/2)),{x:s,y:a}}function W(t,e){const o=t.scales,n=o[Y(o,e,"xScaleID")],r=o[Y(o,e,"yScaleID")];if(!n&&!r)return{};let{left:i,right:s}=n||t.chartArea,{top:a,bottom:d}=r||t.chartArea;const l=N(n,{min:e.xMin,max:e.xMax,start:i,end:s});i=l.start,s=l.end;const c=N(r,{min:e.yMin,max:e.yMax,start:d,end:a});return a=c.start,d=c.end,{x:i,y:a,x2:s,y2:d,width:s-i,height:d-a,centerX:i+(s-i)/2,centerY:a+(d-a)/2}}function _(t,e){if(!b(e)){const o=W(t,e);let n=e.radius;n&&!isNaN(n)||(n=Math.min(o.width,o.height)/2,e.radius=n);const r=2*n,i=o.centerX+e.xAdjust,s=o.centerY+e.yAdjust;return{x:i-n,y:s-n,x2:i+n,y2:s+n,centerX:i,centerY:s,width:r,height:r,radius:n}}return function(t,e){const o=E(t,e),n=2*e.radius;return{x:o.x-e.radius+e.xAdjust,y:o.y-e.radius+e.yAdjust,x2:o.x+e.radius+e.xAdjust,y2:o.y+e.radius+e.yAdjust,centerX:o.x+e.xAdjust,centerY:o.y+e.yAdjust,radius:e.radius,width:n,height:n}}(t,e)}function z(t,e){const{scales:o,chartArea:n}=t,r=o[e.scaleID],i={x:n.left,y:n.top,x2:n.right,y2:n.bottom};return r?function(t,e,o){const n=R(t,o.value,NaN),r=R(t,o.endValue,n);t.isHorizontal()?(e.x=n,e.x2=r):(e.y=n,e.y2=r)}(r,i,e):function(t,e,o){for(const n of Object.keys(T)){const r=t[Y(t,o,n)];if(r){const{min:t,max:i,start:s,end:a,startProp:d,endProp:l}=T[n],c=X(r,{min:o[t],max:o[i],start:r[s],end:r[a]});e[d]=c.start,e[l]=c.end}}}(o,i,e),i}function F(t,e,o){const n=W(t,e);return n.initProperties=g(t,n,e,o),n.elements=[{type:"label",optionScope:"label",properties:V(t,n,e),initProperties:n.initProperties}],n}function N(t,e){const o=X(t,e)||e;return{start:Math.min(o.start,o.end),end:Math.max(o.start,o.end)}}function H(t,e){const{start:o,end:n,borderWidth:r}=t,{position:i,padding:{start:s,end:a},adjust:d}=e;return o+r/2+d+x(n-r-o-s-a-e.size,i)}function V(t,o,n){const r=n.label;r.backgroundColor="transparent",r.callout.display=!1;const i=p(r.position),s=e.toPadding(r.padding),a=A(t.ctx,r),d=function({properties:t,options:e},o,n,r){const{x:i,x2:s,width:a}=t;return H({start:i,end:s,size:a,borderWidth:e.borderWidth},{position:n.x,padding:{start:r.left,end:r.right},adjust:e.label.xAdjust,size:o.width})}({properties:o,options:n},a,i,s),l=function({properties:t,options:e},o,n,r){const{y:i,y2:s,height:a}=t;return H({start:i,end:s,size:a,borderWidth:e.borderWidth},{position:n.y,padding:{start:r.top,end:r.bottom},adjust:e.label.yAdjust,size:o.height})}({properties:o,options:n},a,i,s),c=a.width+s.width,h=a.height+s.height;return{x:d,y:l,x2:d+c,y2:l+h,width:c,height:h,centerX:d+c/2,centerY:l+h/2,rotation:r.rotation}}function B(t,e,o){const n=Math.cos(o),r=Math.sin(o),i=e.x,s=e.y;return{x:i+n*(t.x-i)-r*(t.y-s),y:s+r*(t.x-i)+n*(t.y-s)}}const L=["enter","leave"],$=L.concat("click");function U(t,e,o){if(t.listened)switch(e.type){case"mousemove":case"mouseout":return function(t,e,o){if(!t.moveListened)return;let r;r="mousemove"===e.type?n(t,e,o.interaction):[];const i=t.hovered;t.hovered=r;const s={state:t,event:e};let a=J(s,"leave",i,r);return J(s,"enter",r,i)||a}(t,e,o);case"click":return function(t,e,o){const r=t.listeners,i=n(t,e,o.interaction);let s;for(const t of i)s=Q(t.options.click||r.click,t,e)||s;return s}(t,e,o)}}function J({state:t,event:e},o,n,r){let i;for(const s of n)r.indexOf(s)<0&&(i=Q(s.options[o]||t.listeners[o],s,e)||i);return i}function Q(t,o,n){return!0===e.callback(t,[o.$context,n])}const q=["afterDraw","beforeDraw"];function G(t,o,n){if(t.hooked){const r=o.options[n]||t.hooks[n];return e.callback(r,[o.$context])}}function K(t,o,n){const r=function(t,o,n){const r=o.axis,i=o.id,s=r+"ScaleID",a={min:e.valueOrDefault(o.min,Number.NEGATIVE_INFINITY),max:e.valueOrDefault(o.max,Number.POSITIVE_INFINITY)};for(const e of n)e.scaleID===i?ot(e,o,["value","endValue"],a):Y(t,e,s)===i&&ot(e,o,[r+"Min",r+"Max",r+"Value"],a);return a}(t.scales,o,n);let i=Z(o,r,"min","suggestedMin");i=Z(o,r,"max","suggestedMax")||i,i&&e.isFunction(o.handleTickRangeOptions)&&o.handleTickRangeOptions()}function Z(t,o,n,r){if(e.isFinite(o[n])&&!function(t,o,n){return e.defined(t[o])||e.defined(t[n])}(t.options,n,r)){const e=t[n]!==o[n];return t[n]=o[n],e}}function tt(t,e){for(const o of["scaleID","xScaleID","yScaleID"]){const n=Y(e,t,o);n&&!e[n]&&et(t,o)&&console.warn(`No scale found with id '${n}' for annotation '${t.id}'`)}}function et(t,o){if("scaleID"===o)return!0;const n=o.charAt(0);for(const o of["Min","Max","Value"])if(e.defined(t[n+o]))return!0;return!1}function ot(t,o,n,r){for(const i of n){const n=t[i];if(e.defined(n)){const t=o.parse(n);r.min=Math.min(r.min,t),r.max=Math.max(r.max,t)}}}class nt extends t.Element{inRange(t,o,n,r){const{x:i,y:s}=B({x:t,y:o},this.getCenterPoint(r),e.toRadians(-this.options.rotation));return l({x:i,y:s},this.getProps(["x","y","x2","y2"],r),n,this.options.borderWidth)}getCenterPoint(t){return c(this,t)}draw(t){t.save(),C(t,this.getCenterPoint(),this.options.rotation),j(t,this,this.options),t.restore()}get label(){return this.elements&&this.elements[0]}resolveElementProperties(t,e){return F(t,e)}}nt.id="boxAnnotation",nt.defaults={adjustScaleRange:!0,backgroundShadowColor:"transparent",borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderRadius:0,borderShadowColor:"transparent",borderWidth:1,display:!0,init:void 0,label:{backgroundColor:"transparent",borderWidth:0,callout:{display:!1},color:"black",content:null,display:!1,drawTime:void 0,font:{family:void 0,lineHeight:void 0,size:void 0,style:void 0,weight:"bold"},height:void 0,opacity:void 0,padding:6,position:"center",rotation:void 0,textAlign:"start",textStrokeColor:void 0,textStrokeWidth:0,width:void 0,xAdjust:0,yAdjust:0,z:void 0},rotation:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,xMax:void 0,xMin:void 0,xScaleID:void 0,yMax:void 0,yMin:void 0,yScaleID:void 0,z:0},nt.defaultRoutes={borderColor:"color",backgroundColor:"color"},nt.descriptors={label:{_fallback:!0}};const rt=["left","bottom","top","right"];class it extends t.Element{inRange(t,o,n,r){const{x:i,y:s}=B({x:t,y:o},this.getCenterPoint(r),e.toRadians(-this.rotation));return l({x:i,y:s},this.getProps(["x","y","x2","y2"],r),n,this.options.borderWidth)}getCenterPoint(t){return c(this,t)}draw(t){const o=this.options,n=!e.defined(this._visible)||this._visible;o.display&&o.content&&n&&(t.save(),C(t,this.getCenterPoint(),this.rotation),function(t,o){const{pointX:n,pointY:r,options:i}=o,s=i.callout,a=s&&s.display&&function(t,o){const n=o.position;if(rt.includes(n))return n;return function(t,o){const{x:n,y:r,x2:i,y2:s,width:a,height:d,pointX:l,pointY:c,centerX:h,centerY:u,rotation:f}=t,x={x:h,y:u},p=o.start,b=y(a,p),g=y(d,p),m=[n,n+b,n+b,i],v=[r+g,s,r,s],w=[];for(let t=0;t<4;t++){const o=B({x:m[t],y:v[t]},x,e.toRadians(f));w.push({position:rt[t],distance:e.distanceBetweenPoints(o,{x:l,y:c})})}return w.sort(((t,e)=>t.distance-e.distance))[0].position}(t,o)}(o,s);if(!a||function(t,e,o){const{pointX:n,pointY:r}=t,i=e.margin;let s=n,a=r;"left"===o?s+=i:"right"===o?s-=i:"top"===o?a+=i:"bottom"===o&&(a-=i);return t.inRange(s,a)}(o,s,a))return;t.save(),t.beginPath();const d=k(t,s);if(!d)return t.restore();const{separatorStart:l,separatorEnd:c}=function(t,e){const{x:o,y:n,x2:r,y2:i}=t,s=function(t,e){const{width:o,height:n,options:r}=t,i=r.callout.margin+r.borderWidth/2;if("right"===e)return o+i;if("bottom"===e)return n+i;return-i}(t,e);let a,d;"left"===e||"right"===e?(a={x:o+s,y:n},d={x:a.x,y:i}):(a={x:o,y:n+s},d={x:r,y:a.y});return{separatorStart:a,separatorEnd:d}}(o,a),{sideStart:h,sideEnd:u}=function(t,e,o){const{y:n,width:r,height:i,options:s}=t,a=s.callout.start,d=function(t,e){const o=e.side;if("left"===t||"top"===t)return-o;return o}(e,s.callout);let l,c;"left"===e||"right"===e?(l={x:o.x,y:n+y(i,a)},c={x:l.x+d,y:l.y}):(l={x:o.x+y(r,a),y:o.y},c={x:l.x,y:l.y+d});return{sideStart:l,sideEnd:c}}(o,a,l);(s.margin>0||0===i.borderWidth)&&(t.moveTo(l.x,l.y),t.lineTo(c.x,c.y));t.moveTo(h.x,h.y),t.lineTo(u.x,u.y);const f=B({x:n,y:r},o.getCenterPoint(),e.toRadians(-o.rotation));t.lineTo(f.x,f.y),t.stroke(),t.restore()}(t,this),j(t,this,o),I(t,function({x:t,y:o,width:n,height:r,options:i}){const s=i.borderWidth/2,a=e.toPadding(i.padding);return{x:t+a.left+s,y:o+a.top+s,width:n-a.left-a.right-i.borderWidth,height:r-a.top-a.bottom-i.borderWidth}}(this),o),t.restore())}resolveElementProperties(t,o){let n;if(b(o))n=E(t,o);else{const{centerX:e,centerY:r}=W(t,o);n={x:e,y:r}}const r=e.toPadding(o.padding),i=function(t,e,o,n){const r=e.width+n.width+o.borderWidth,i=e.height+n.height+o.borderWidth,s=p(o.position,"center"),a=st(t.x,r,o.xAdjust,s.x),d=st(t.y,i,o.yAdjust,s.y);return{x:a,y:d,x2:a+r,y2:d+i,width:r,height:i,centerX:a+r/2,centerY:d+i/2}}(n,A(t.ctx,o),o,r);return{initProperties:g(t,i,o),pointX:n.x,pointY:n.y,...i,rotation:o.rotation}}}function st(t,e,o=0,n){return t-x(e,n)+o}it.id="labelAnnotation",it.defaults={adjustScaleRange:!0,backgroundColor:"transparent",backgroundShadowColor:"transparent",borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderRadius:0,borderShadowColor:"transparent",borderWidth:0,callout:{borderCapStyle:"butt",borderColor:void 0,borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:1,display:!1,margin:5,position:"auto",side:5,start:"50%"},color:"black",content:null,display:!0,font:{family:void 0,lineHeight:void 0,size:void 0,style:void 0,weight:void 0},height:void 0,init:void 0,opacity:void 0,padding:6,position:"center",rotation:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,textAlign:"center",textStrokeColor:void 0,textStrokeWidth:0,width:void 0,xAdjust:0,xMax:void 0,xMin:void 0,xScaleID:void 0,xValue:void 0,yAdjust:0,yMax:void 0,yMin:void 0,yScaleID:void 0,yValue:void 0,z:0},it.defaultRoutes={borderColor:"color"};const at=(t,e,o)=>({x:t.x+o*(e.x-t.x),y:t.y+o*(e.y-t.y)}),dt=(t,e,o)=>at(e,o,Math.abs((t-e.y)/(o.y-e.y))).x,lt=(t,e,o)=>at(e,o,Math.abs((t-e.x)/(o.x-e.x))).y,ct=t=>t*t,ht=(t,e,{x:o,y:n,x2:r,y2:i},s)=>"y"===s?{start:Math.min(n,i),end:Math.max(n,i),value:e}:{start:Math.min(o,r),end:Math.max(o,r),value:t},ut=(t,e,o,n)=>(1-n)*(1-n)*t+2*(1-n)*n*e+n*n*o,ft=(t,e,o,n)=>({x:ut(t.x,e.x,o.x,n),y:ut(t.y,e.y,o.y,n)}),xt=(t,e,o,n)=>2*(1-n)*(e-t)+2*n*(o-e),yt=(t,o,n,r)=>-Math.atan2(xt(t.x,o.x,n.x,r),xt(t.y,o.y,n.y,r))+.5*e.PI;class pt extends t.Element{inRange(t,e,o,n){const r=this.options.borderWidth/2;if("x"!==o&&"y"!==o){const o={mouseX:t,mouseY:e},{path:i,ctx:a}=this;if(i){k(a,this.options);const{chart:r}=this.$context,s=t*r.currentDevicePixelRatio,d=e*r.currentDevicePixelRatio,l=a.isPointInStroke(i,s,d)||mt(this,o,n);return a.restore(),l}return function(t,{mouseX:e,mouseY:o},n=s,r){const{x:i,y:a,x2:d,y2:l}=t.getProps(["x","y","x2","y2"],r),c=d-i,h=l-a,u=ct(c)+ct(h),f=0===u?-1:((e-i)*c+(o-a)*h)/u;let x,y;f<0?(x=i,y=a):f>1?(x=d,y=l):(x=i+f*c,y=a+f*h);return ct(e-x)+ct(o-y)<=n}(this,o,ct(r),n)||mt(this,o,n)}return function(t,{mouseX:e,mouseY:o},n,{hBorderWidth:r,useFinalPosition:i}){const s=ht(e,o,t.getProps(["x","y","x2","y2"],i),n);return s.value>=s.start-r&&s.value<=s.end+r||mt(t,{mouseX:e,mouseY:o},i,n)}(this,{mouseX:t,mouseY:e},o,{hBorderWidth:r,useFinalPosition:n})}getCenterPoint(t){return c(this,t)}draw(t){const{x:o,y:n,x2:r,y2:i,cp:s,options:a}=this;if(t.save(),!k(t,a))return t.restore();D(t,a);const d=Math.sqrt(Math.pow(r-o,2)+Math.pow(i-n,2));if(a.curve&&s)return function(t,o,n,r){const{x:i,y:s,x2:a,y2:d,options:l}=o,{startOpts:c,endOpts:h,startAdjust:u,endAdjust:f}=Mt(o),x={x:i,y:s},y={x:a,y:d},p=yt(x,n,y,0),b=yt(x,n,y,1)-e.PI,g=ft(x,n,y,u/r),m=ft(x,n,y,1-f/r),v=new Path2D;t.beginPath(),v.moveTo(g.x,g.y),v.quadraticCurveTo(n.x,n.y,m.x,m.y),t.shadowColor=l.borderShadowColor,t.stroke(v),o.path=v,o.ctx=t,Ct(t,g,{angle:p,adjust:u},c),Ct(t,m,{angle:b,adjust:f},h)}(t,this,s,d),t.restore();const{startOpts:l,endOpts:c,startAdjust:h,endAdjust:u}=Mt(this),f=Math.atan2(i-n,r-o);t.translate(o,n),t.rotate(f),t.beginPath(),t.moveTo(0+h,0),t.lineTo(d-u,0),t.shadowColor=a.borderShadowColor,t.stroke(),St(t,0,h,l),St(t,d,-u,c),t.restore()}get label(){return this.elements&&this.elements[0]}resolveElementProperties(t,o){const n=z(t,o),{x:r,y:i,x2:s,y2:a}=n,d=function({x:t,y:e,x2:o,y2:n},{top:r,right:i,bottom:s,left:a}){return!(t<a&&o<a||t>i&&o>i||e<r&&n<r||e>s&&n>s)}(n,t.chartArea),l=d?function(t,e,o){const{x:n,y:r}=gt(t,e,o),{x:i,y:s}=gt(e,t,o);return{x:n,y:r,x2:i,y2:s,width:Math.abs(i-n),height:Math.abs(s-r)}}({x:r,y:i},{x:s,y:a},t.chartArea):{x:r,y:i,x2:s,y2:a,width:Math.abs(s-r),height:Math.abs(a-i)};if(l.centerX=(s+r)/2,l.centerY=(a+i)/2,l.initProperties=g(t,l,o),o.curve){const t={x:l.x,y:l.y},n={x:l.x2,y:l.y2};l.cp=function(t,e,o){const{x:n,y:r,x2:i,y2:s,centerX:a,centerY:d}=t,l=Math.atan2(s-r,i-n),c=p(e.controlPoint,0);return B({x:a+y(o,c.x,!1),y:d+y(o,c.y,!1)},{x:a,y:d},l)}(l,o,e.distanceBetweenPoints(t,n))}const c=function(t,o,n){const r=n.borderWidth,i=e.toPadding(n.padding),s=A(t.ctx,n),a=s.width+i.width+r,d=s.height+i.height+r;return function(t,o,n,r){const{width:i,height:s,padding:a}=n,{xAdjust:d,yAdjust:l}=o,c={x:t.x,y:t.y},h={x:t.x2,y:t.y2},u="auto"===o.rotation?function(t){const{x:o,y:n,x2:r,y2:i}=t,s=Math.atan2(i-n,r-o);return s>e.PI/2?s-e.PI:s<e.PI/-2?s+e.PI:s}(t):e.toRadians(o.rotation),f=function(t,e,o){const n=Math.cos(o),r=Math.sin(o);return{w:Math.abs(t*n)+Math.abs(e*r),h:Math.abs(t*r)+Math.abs(e*n)}}(i,s,u),y=function(t,e,o,n){let r;const i=function(t,e){const{x:o,x2:n,y:r,y2:i}=t,s=Math.min(r,i)-e.top,a=Math.min(o,n)-e.left,d=e.bottom-Math.max(r,i),l=e.right-Math.max(o,n);return{x:Math.min(a,l),y:Math.min(s,d),dx:a<=l?1:-1,dy:s<=d?1:-1}}(t,n);r="start"===e.position?vt({w:t.x2-t.x,h:t.y2-t.y},o,e,i):"end"===e.position?1-vt({w:t.x-t.x2,h:t.y-t.y2},o,e,i):x(1,e.position);return r}(t,o,{labelSize:f,padding:a},r),p=t.cp?ft(c,t.cp,h,y):at(c,h,y),b={size:f.w,min:r.left,max:r.right,padding:a.left},g={size:f.h,min:r.top,max:r.bottom,padding:a.top},m=wt(p.x,b)+d,v=wt(p.y,g)+l;return{x:m-i/2,y:v-s/2,x2:m+i/2,y2:v+s/2,centerX:m,centerY:v,pointX:p.x,pointY:p.y,width:i,height:s,rotation:e.toDegrees(u)}}(o,n,{width:a,height:d,padding:i},t.chartArea)}(t,l,o.label);return c._visible=d,l.elements=[{type:"label",optionScope:"label",properties:c,initProperties:l.initProperties}],l}}pt.id="lineAnnotation";const bt={backgroundColor:void 0,backgroundShadowColor:void 0,borderColor:void 0,borderDash:void 0,borderDashOffset:void 0,borderShadowColor:void 0,borderWidth:void 0,display:void 0,fill:void 0,length:void 0,shadowBlur:void 0,shadowOffsetX:void 0,shadowOffsetY:void 0,width:void 0};function gt({x:t,y:e},o,{top:n,right:r,bottom:i,left:s}){return t<s&&(e=lt(s,{x:t,y:e},o),t=s),t>r&&(e=lt(r,{x:t,y:e},o),t=r),e<n&&(t=dt(n,{x:t,y:e},o),e=n),e>i&&(t=dt(i,{x:t,y:e},o),e=i),{x:t,y:e}}function mt(t,{mouseX:e,mouseY:o},n,r){const i=t.label;return i.options.display&&i.inRange(e,o,r,n)}function vt(t,e,o,n){const{labelSize:r,padding:i}=e,s=t.w*n.dx,d=t.h*n.dy,l=s>0&&(r.w/2+i.left-n.x)/s,c=d>0&&(r.h/2+i.top-n.y)/d;return a(Math.max(l,c),0,.25)}function wt(t,e){const{size:o,min:n,max:r,padding:i}=e,s=o/2;return o>r-n?(r+n)/2:(n>=t-i-s&&(t=n+i+s),r<=t+i+s&&(t=r-i-s),t)}function Mt(t){const e=t.options,o=e.arrowHeads&&e.arrowHeads.start,n=e.arrowHeads&&e.arrowHeads.end;return{startOpts:o,endOpts:n,startAdjust:Pt(t,o),endAdjust:Pt(t,n)}}function Pt(t,e){if(!e||!e.display)return 0;const{length:o,width:n}=e,r=t.options.borderWidth/2,i={x:o,y:n+r},s={x:0,y:r};return Math.abs(dt(0,i,s))}function St(t,e,o,n){if(!n||!n.display)return;const{length:r,width:i,fill:s,backgroundColor:a,borderColor:d}=n,l=Math.abs(e-r)+o;t.beginPath(),D(t,n),k(t,n),t.moveTo(l,-i),t.lineTo(e+o,0),t.lineTo(l,i),!0===s?(t.fillStyle=a||d,t.closePath(),t.fill(),t.shadowColor="transparent"):t.shadowColor=n.borderShadowColor,t.stroke()}function Ct(t,{x:e,y:o},{angle:n,adjust:r},i){i&&i.display&&(t.save(),t.translate(e,o),t.rotate(n),St(t,0,-r,i),t.restore())}pt.defaults={adjustScaleRange:!0,arrowHeads:{display:!1,end:Object.assign({},bt),fill:!1,length:12,start:Object.assign({},bt),width:6},borderDash:[],borderDashOffset:0,borderShadowColor:"transparent",borderWidth:2,curve:!1,controlPoint:{y:"-50%"},display:!0,endValue:void 0,init:void 0,label:{backgroundColor:"rgba(0,0,0,0.8)",backgroundShadowColor:"transparent",borderCapStyle:"butt",borderColor:"black",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderRadius:6,borderShadowColor:"transparent",borderWidth:0,callout:Object.assign({},it.defaults.callout),color:"#fff",content:null,display:!1,drawTime:void 0,font:{family:void 0,lineHeight:void 0,size:void 0,style:void 0,weight:"bold"},height:void 0,opacity:void 0,padding:6,position:"center",rotation:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,textAlign:"center",textStrokeColor:void 0,textStrokeWidth:0,width:void 0,xAdjust:0,yAdjust:0,z:void 0},scaleID:void 0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,value:void 0,xMax:void 0,xMin:void 0,xScaleID:void 0,yMax:void 0,yMin:void 0,yScaleID:void 0,z:0},pt.descriptors={arrowHeads:{start:{_fallback:!0},end:{_fallback:!0},_fallback:!0}},pt.defaultRoutes={borderColor:"color"};class kt extends t.Element{inRange(t,o,n,r){const i=this.options.rotation,a=this.options.borderWidth;if("x"!==n&&"y"!==n)return function(t,o,n,r){const{width:i,height:s,centerX:a,centerY:d}=o,l=i/2,c=s/2;if(l<=0||c<=0)return!1;const h=e.toRadians(n||0),u=r/2||0,f=Math.cos(h),x=Math.sin(h),y=Math.pow(f*(t.x-a)+x*(t.y-d),2),p=Math.pow(x*(t.x-a)-f*(t.y-d),2);return y/Math.pow(l+u,2)+p/Math.pow(c+u,2)<=1.0001}({x:t,y:o},this.getProps(["width","height","centerX","centerY"],r),i,a);const{x:d,y:l,x2:c,y2:h}=this.getProps(["x","y","x2","y2"],r),u=a/2,f="y"===n?{start:l,end:h}:{start:d,end:c},x=B({x:t,y:o},this.getCenterPoint(r),e.toRadians(-i));return x[n]>=f.start-u-s&&x[n]<=f.end+u+s}getCenterPoint(t){return c(this,t)}draw(t){const{width:o,height:n,centerX:r,centerY:i,options:s}=this;t.save(),C(t,this.getCenterPoint(),s.rotation),D(t,this.options),t.beginPath(),t.fillStyle=s.backgroundColor;const a=k(t,s);t.ellipse(r,i,n/2,o/2,e.PI/2,0,2*e.PI),t.fill(),a&&(t.shadowColor=s.borderShadowColor,t.stroke()),t.restore()}get label(){return this.elements&&this.elements[0]}resolveElementProperties(t,e){return F(t,e,!0)}}kt.id="ellipseAnnotation",kt.defaults={adjustScaleRange:!0,backgroundShadowColor:"transparent",borderDash:[],borderDashOffset:0,borderShadowColor:"transparent",borderWidth:1,display:!0,init:void 0,label:Object.assign({},nt.defaults.label),rotation:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,xMax:void 0,xMin:void 0,xScaleID:void 0,yMax:void 0,yMin:void 0,yScaleID:void 0,z:0},kt.defaultRoutes={borderColor:"color",backgroundColor:"color"},kt.descriptors={label:{_fallback:!0}};class Dt extends t.Element{inRange(t,e,o,n){const{x:r,y:i,x2:s,y2:a,width:d}=this.getProps(["x","y","x2","y2","width"],n),l=this.options.borderWidth;if("x"!==o&&"y"!==o)return function(t,e,o,n){if(!t||!e||o<=0)return!1;const r=n/2;return Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2)<=Math.pow(o+r,2)}({x:t,y:e},this.getCenterPoint(n),d/2,l);const c=l/2,h="y"===o?{start:i,end:a,value:e}:{start:r,end:s,value:t};return h.value>=h.start-c&&h.value<=h.end+c}getCenterPoint(t){return c(this,t)}draw(t){const e=this.options,o=e.borderWidth;if(e.radius<.1)return;t.save(),t.fillStyle=e.backgroundColor,D(t,e);const n=k(t,e);O(t,this,this.centerX,this.centerY),n&&!S(e.pointStyle)&&(t.shadowColor=e.borderShadowColor,t.stroke()),t.restore(),e.borderWidth=o}resolveElementProperties(t,e){const o=_(t,e);return o.initProperties=g(t,o,e,!0),o}}Dt.id="pointAnnotation",Dt.defaults={adjustScaleRange:!0,backgroundShadowColor:"transparent",borderDash:[],borderDashOffset:0,borderShadowColor:"transparent",borderWidth:1,display:!0,init:void 0,pointStyle:"circle",radius:10,rotation:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,xAdjust:0,xMax:void 0,xMin:void 0,xScaleID:void 0,xValue:void 0,yAdjust:0,yMax:void 0,yMin:void 0,yScaleID:void 0,yValue:void 0,z:0},Dt.defaultRoutes={borderColor:"color",backgroundColor:"color"};class At extends t.Element{inRange(t,o,n,r){if("x"!==n&&"y"!==n)return this.options.radius>=.1&&this.elements.length>1&&function(t,e,o,n){let r=!1,i=t[t.length-1].getProps(["bX","bY"],n);for(const s of t){const t=s.getProps(["bX","bY"],n);t.bY>o!=i.bY>o&&e<(i.bX-t.bX)*(o-t.bY)/(i.bY-t.bY)+t.bX&&(r=!r),i=t}return r}(this.elements,t,o,r);const i=B({x:t,y:o},this.getCenterPoint(r),e.toRadians(-this.options.rotation)),s=this.elements.map((t=>"y"===n?t.bY:t.bX)),a=Math.min(...s),d=Math.max(...s);return i[n]>=a&&i[n]<=d}getCenterPoint(t){return c(this,t)}draw(t){const{elements:e,options:o}=this;t.save(),t.beginPath(),t.fillStyle=o.backgroundColor,D(t,o);const n=k(t,o);let r=!0;for(const o of e)r?(t.moveTo(o.x,o.y),r=!1):t.lineTo(o.x,o.y);t.closePath(),t.fill(),n&&(t.shadowColor=o.borderShadowColor,t.stroke()),t.restore()}resolveElementProperties(t,o){const n=_(t,o),{sides:r,rotation:i}=o,s=[],a=2*e.PI/r;let d=i*e.RAD_PER_DEG;for(let e=0;e<r;e++,d+=a){const e=jt(n,o,d);e.initProperties=g(t,n,o),s.push(e)}return n.elements=s,n}}function jt({centerX:t,centerY:e},{radius:o,borderWidth:n},r){const i=n/2,s=Math.sin(r),a=Math.cos(r),d={x:t+s*o,y:e-a*o};return{type:"point",optionScope:"point",properties:{x:d.x,y:d.y,centerX:d.x,centerY:d.y,bX:t+s*(o+i),bY:e-a*(o+i)}}}At.id="polygonAnnotation",At.defaults={adjustScaleRange:!0,backgroundShadowColor:"transparent",borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderShadowColor:"transparent",borderWidth:1,display:!0,init:void 0,point:{radius:0},radius:10,rotation:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,sides:3,xAdjust:0,xMax:void 0,xMin:void 0,xScaleID:void 0,xValue:void 0,yAdjust:0,yMax:void 0,yMin:void 0,yScaleID:void 0,yValue:void 0,z:0},At.defaultRoutes={borderColor:"color",backgroundColor:"color"};const It={box:nt,ellipse:kt,label:it,line:pt,point:Dt,polygon:At};Object.keys(It).forEach((e=>{t.defaults.describe(`elements.${It[e].id}`,{_fallback:"plugins.annotation.common"})}));const Ot={update:Object.assign},Tt=$.concat(q),Rt=(t,o)=>e.isObject(o)?Nt(t,o):t,Yt=t=>"color"===t||"font"===t;function Xt(t="line"){return It[t]?t:(console.warn(`Unknown annotation type: '${t}', defaulting to 'line'`),"line")}function Et(o,n,r,i){const s=function(e,o,n){if("reset"===n||"none"===n||"resize"===n)return Ot;return new t.Animations(e,o)}(o,r.animations,i),a=n.annotations,d=function(t,e){const o=e.length,n=t.length;if(n<o){const e=o-n;t.splice(n,0,...new Array(e))}else n>o&&t.splice(o,n-o);return t}(n.elements,a);for(let t=0;t<a.length;t++){const n=a[t],r=zt(d,t,n.type),i=n.setContext(Ht(o,r,n)),l=r.resolveElementProperties(o,i);l.skip=Wt(l),"elements"in l&&(_t(r,l.elements,i,s),delete l.elements),e.defined(r.x)||Object.assign(r,l),Object.assign(r,l.initProperties),l.options=Ft(i),s.update(r,l)}}function Wt(t){return isNaN(t.x)||isNaN(t.y)}function _t(t,e,o,n){const r=t.elements||(t.elements=[]);r.length=e.length;for(let t=0;t<e.length;t++){const i=e[t],s=i.properties,a=zt(r,t,i.type,i.initProperties),d=o[i.optionScope].override(i);s.options=Ft(d),n.update(a,s)}}function zt(t,e,o,n){const r=It[Xt(o)];let i=t[e];return i&&i instanceof r||(i=t[e]=new r,Object.assign(i,n)),i}function Ft(t){const e=It[Xt(t.type)],o={};o.id=t.id,o.type=t.type,o.drawTime=t.drawTime,Object.assign(o,Nt(t,e.defaults),Nt(t,e.defaultRoutes));for(const e of Tt)o[e]=t[e];return o}function Nt(t,o){const n={};for(const r of Object.keys(o)){const i=o[r],s=t[r];Yt(r)&&e.isArray(s)?n[r]=s.map((t=>Rt(t,i))):n[r]=Rt(s,i)}return n}function Ht(t,e,o){return e.$context||(e.$context=Object.assign(Object.create(t.getContext()),{element:e,id:o.id,type:"annotation"}))}const Vt=new Map,Bt=$.concat(q);var Lt={id:"annotation",version:"2.2.1",beforeRegister(){!function(t,e,o,n=!0){const r=o.split(".");let s=0;for(const a of e.split(".")){const d=r[s++];if(parseInt(a,10)<parseInt(d,10))break;if(i(d,a)){if(n)throw new Error(`${t} v${o} is not supported. v${e} or newer is required.`);return!1}}}("chart.js","3.7",t.Chart.version)},afterRegister(){t.Chart.register(It)},afterUnregister(){t.Chart.unregister(It)},beforeInit(t){Vt.set(t,{annotations:[],elements:[],visibleElements:[],listeners:{},listened:!1,moveListened:!1,hooks:{},hooked:!1,hovered:[]})},beforeUpdate(t,o,n){const r=Vt.get(t).annotations=[];let i=n.annotations;e.isObject(i)?Object.keys(i).forEach((t=>{const o=i[t];e.isObject(o)&&(o.id=t,r.push(o))})):e.isArray(i)&&r.push(...i),function(t,e){for(const o of t)tt(o,e)}(r,t.scales)},afterDataLimits(t,e){const o=Vt.get(t);K(t,e.scale,o.annotations.filter((t=>t.display&&t.adjustScaleRange)))},afterUpdate(t,o,r){const i=Vt.get(t);!function(t,o,r){o.listened=m(r,$,o.listeners),o.moveListened=!1,o._getElements=n,L.forEach((t=>{e.isFunction(r[t])&&(o.moveListened=!0)})),o.listened&&o.moveListened||o.annotations.forEach((t=>{!o.listened&&e.isFunction(t.click)&&(o.listened=!0),o.moveListened||L.forEach((n=>{e.isFunction(t[n])&&(o.listened=!0,o.moveListened=!0)}))}))}(0,i,r),Et(t,i,r,o.mode),i.visibleElements=i.elements.filter((t=>!t.skip&&t.options.display)),function(t,o,n){const r=o.visibleElements;o.hooked=m(n,q,o.hooks),o.hooked||r.forEach((t=>{o.hooked||q.forEach((n=>{e.isFunction(t.options[n])&&(o.hooked=!0)}))}))}(0,i,r)},beforeDatasetsDraw(t,e,o){$t(t,"beforeDatasetsDraw",o.clip)},afterDatasetsDraw(t,e,o){$t(t,"afterDatasetsDraw",o.clip)},beforeDraw(t,e,o){$t(t,"beforeDraw",o.clip)},afterDraw(t,e,o){$t(t,"afterDraw",o.clip)},beforeEvent(t,e,o){U(Vt.get(t),e.event,o)&&(e.changed=!0)},afterDestroy(t){Vt.delete(t)},_getState:t=>Vt.get(t),defaults:{animations:{numbers:{properties:["x","y","x2","y2","width","height","centerX","centerY","pointX","pointY","radius"],type:"number"}},clip:!0,interaction:{mode:void 0,axis:void 0,intersect:void 0},common:{drawTime:"afterDatasetsDraw",init:!1,label:{}}},descriptors:{_indexable:!1,_scriptable:t=>!Bt.includes(t)&&"init"!==t,annotations:{_allKeys:!1,_fallback:(t,e)=>`elements.${It[Xt(e.type)].id}`},interaction:{_fallback:!0},common:{label:{_indexable:Yt,_fallback:!0},_indexable:Yt}},additionalOptionScopes:[""]};function $t(t,o,n){const{ctx:r,chartArea:i}=t,s=Vt.get(t);n&&e.clipArea(r,i);const a=function(t,e){const o=[];for(const n of t)if(n.options.drawTime===e&&o.push({element:n,main:!0}),n.elements&&n.elements.length)for(const t of n.elements)t.options.display&&t.options.drawTime===e&&o.push({element:t});return o}(s.visibleElements,o).sort(((t,e)=>t.element.options.z-e.element.options.z));for(const t of a)Ut(r,i,s,t);n&&e.unclipArea(r)}function Ut(t,e,o,n){const r=n.element;n.main?(G(o,r,"beforeDraw"),r.draw(t,e),G(o,r,"afterDraw")):r.draw(t,e)}return t.Chart.register(Lt),Lt})); diff --git a/vendor/assets/stylesheets/jquery-ui-1.10.0.custom.scss b/vendor/assets/stylesheets/jquery-ui-1.10.0.custom.scss index 6d9b5eee5..1643a6fef 100644 --- a/vendor/assets/stylesheets/jquery-ui-1.10.0.custom.scss +++ b/vendor/assets/stylesheets/jquery-ui-1.10.0.custom.scss @@ -11,9 +11,13 @@ /* Layout helpers ----------------------------------*/ -.ui-helper-hidden { display: none; } +.ui-helper-hidden { + display: none; +} + .ui-helper-hidden-accessible { - border: 0; clip: rect(0 0 0 0); + border: 0; + clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; @@ -21,6 +25,7 @@ position: absolute; width: 1px; } + .ui-helper-reset { margin: 0; padding: 0; @@ -31,11 +36,13 @@ font-size: 100%; list-style: none; } + .ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; } + .ui-helper-clearfix:after { content: "."; display: block; @@ -43,14 +50,17 @@ clear: both; visibility: hidden; } + .ui-helper-clearfix { /*display: inline-block; */ - display:block; - min-height: 0; /* support: IE7 */ + display: block; + min-height: 0; + /* support: IE7 */ } + /* required comment for clearfix to work in Opera \*/ * html .ui-helper-clearfix { - height:1%; + height: 1%; } /* end clearfix */ @@ -61,8 +71,9 @@ left: 0; position: absolute; opacity: 0; - filter:Alpha(Opacity=0); + filter: Alpha(Opacity=0); } + .ui-front { z-index: 100; } @@ -70,7 +81,9 @@ /* Interaction Cues ----------------------------------*/ -.ui-state-disabled { cursor: default !important; } +.ui-state-disabled { + cursor: default !important; +} /* Icons @@ -209,29 +222,56 @@ /* Component containers ----------------------------------*/ -.ui-widget { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size:13px; } -.ui-widget .ui-widget { font-size: 1em; } -.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 1em; } -.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff image-url("ui-bg_glass_75_ffffff_1x400.png") 50% 50% repeat-x; color: #404040; } -.ui-widget-content a { color: #404040; } +.ui-widget { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; +} + +.ui-widget .ui-widget { + font-size: 1em; +} + +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 1em; +} + +.ui-widget-content { + border: 1px solid #aaaaaa; + background: #ffffff image-url("ui-bg_glass_75_ffffff_1x400.png") 50% 50% repeat-x; + color: #404040; +} + +.ui-widget-content a { + color: #404040; +} + .ui-widget-header { - font-weight:bold; + font-weight: bold; border-color: #238bca #238bca #003f81; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - border:1px solid #666; + border: 1px solid #666; - } -.ui-widget-header a { color: #222222; } +} + +.ui-widget-header a { + color: #222222; +} /* Interaction states ----------------------------------*/ -.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default { background-color: #e6e6e6; background-repeat: no-repeat; background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6)); background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6); + background-image: -moz-linear-gradient(to bottom, #ffffff, #ffffff 25%, #e6e6e6); background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); @@ -252,292 +292,920 @@ -ms-transition: 0.1s linear background-image; -o-transition: 0.1s linear background-image; transition: 0.1s linear background-image; - overflow: visible; + overflow: visible; - } +} + + +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited { + color: #555555; + text-decoration: none; +} + +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus { + background-position: 0 -15px; + color: #333; + text-decoration: none; +} + +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited { + color: #212121; + text-decoration: none; +} + +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active { + border: 1px solid #aaaaaa; + font-weight: normal; + color: #212121; +} + +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #212121; + text-decoration: none; +} + +.ui-widget :active { + outline: none; +} + +/* Interaction Cues +----------------------------------*/ + + +.ui-state-highlight p, +.ui-state-error p, +.ui-state-default p { + font-size: 13px; + font-weight: normal; + line-height: 18px; + margin: 7px 15px; +} + +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + + + position: relative; + margin-bottom: 18px; + color: #404040; + background-color: #eedc94; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94)); + background-image: -moz-linear-gradient(to bottom, #fceec1, #eedc94); + background-image: -ms-linear-gradient(to bottom, #fceec1, #eedc94); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94)); + background-image: -webkit-linear-gradient(to bottom, #fceec1, #eedc94); + background-image: -o-linear-gradient(to bottom, #fceec1, #eedc94); + background-image: linear-gradient(to bottom, #fceec1, #eedc94); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + border-color: #eedc94 #eedc94 #e4c652; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + border-width: 1px; + border-style: solid; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + + +} + +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #363636; +} + +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + + + position: relative; + margin-bottom: 18px; + color: #ffffff; + border-width: 1px; + border-style: solid; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + background-color: #c43c35; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35)); + background-image: -moz-linear-gradient(to bottom, #ee5f5b, #c43c35); + background-image: -ms-linear-gradient(to bottom, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35)); + background-image: -webkit-linear-gradient(to bottom, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(to bottom, #ee5f5b, #c43c35); + background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + border-color: #c43c35 #c43c35 #882a25; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + + +} + +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #cd0a0a; +} + +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #cd0a0a; +} + +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} + +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter: Alpha(Opacity=70); + font-weight: normal; +} + +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter: Alpha(Opacity=35); + background-image: none; +} + +.ui-state-disabled .ui-icon { + filter: Alpha(Opacity=35); +} + +/* For IE8 - See #6059 */ + + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; + background-image: image-url("ui-icons_222222_256x240.png"); +} + +.ui-widget-content .ui-icon { + background-image: image-url("ui-icons_222222_256x240.png"); +} + +.ui-widget-header .ui-icon { + background-image: image-url("ui-icons_222222_256x240.png"); +} + +.ui-state-default .ui-icon { + background-image: image-url("ui-icons_888888_256x240.png"); +} + +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon { + background-image: image-url("ui-icons_454545_256x240.png"); +} + +.ui-state-active .ui-icon { + background-image: image-url("ui-icons_454545_256x240.png"); +} + +.ui-state-highlight .ui-icon { + background-image: image-url("ui-icons_2e83ff_256x240.png"); +} + +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: image-url("ui-icons_f6cf3b_256x240.png"); +} + +/* positioning */ +.ui-icon-carat-1-n { + background-position: 0 0; +} + +.ui-icon-carat-1-ne { + background-position: -16px 0; +} + +.ui-icon-carat-1-e { + background-position: -32px 0; +} + +.ui-icon-carat-1-se { + background-position: -48px 0; +} + +.ui-icon-carat-1-s { + background-position: -64px 0; +} + +.ui-icon-carat-1-sw { + background-position: -80px 0; +} + +.ui-icon-carat-1-w { + background-position: -96px 0; +} + +.ui-icon-carat-1-nw { + background-position: -112px 0; +} + +.ui-icon-carat-2-n-s { + background-position: -128px 0; +} + +.ui-icon-carat-2-e-w { + background-position: -144px 0; +} + +.ui-icon-triangle-1-n { + background-position: 0 -16px; +} + +.ui-icon-triangle-1-ne { + background-position: -16px -16px; +} + +.ui-icon-triangle-1-e { + background-position: -32px -16px; +} + +.ui-icon-triangle-1-se { + background-position: -48px -16px; +} + +.ui-icon-triangle-1-s { + background-position: -64px -16px; +} + +.ui-icon-triangle-1-sw { + background-position: -80px -16px; +} + +.ui-icon-triangle-1-w { + background-position: -96px -16px; +} + +.ui-icon-triangle-1-nw { + background-position: -112px -16px; +} + +.ui-icon-triangle-2-n-s { + background-position: -128px -16px; +} + +.ui-icon-triangle-2-e-w { + background-position: -144px -16px; +} + +.ui-icon-arrow-1-n { + background-position: 0 -32px; +} + +.ui-icon-arrow-1-ne { + background-position: -16px -32px; +} + +.ui-icon-arrow-1-e { + background-position: -32px -32px; +} + +.ui-icon-arrow-1-se { + background-position: -48px -32px; +} + +.ui-icon-arrow-1-s { + background-position: -64px -32px; +} + +.ui-icon-arrow-1-sw { + background-position: -80px -32px; +} + +.ui-icon-arrow-1-w { + background-position: -96px -32px; +} + +.ui-icon-arrow-1-nw { + background-position: -112px -32px; +} + +.ui-icon-arrow-2-n-s { + background-position: -128px -32px; +} + +.ui-icon-arrow-2-ne-sw { + background-position: -144px -32px; +} + +.ui-icon-arrow-2-e-w { + background-position: -160px -32px; +} + +.ui-icon-arrow-2-se-nw { + background-position: -176px -32px; +} + +.ui-icon-arrowstop-1-n { + background-position: -192px -32px; +} + +.ui-icon-arrowstop-1-e { + background-position: -208px -32px; +} + +.ui-icon-arrowstop-1-s { + background-position: -224px -32px; +} + +.ui-icon-arrowstop-1-w { + background-position: -240px -32px; +} + +.ui-icon-arrowthick-1-n { + background-position: 0 -48px; +} + +.ui-icon-arrowthick-1-ne { + background-position: -16px -48px; +} + +.ui-icon-arrowthick-1-e { + background-position: -32px -48px; +} + +.ui-icon-arrowthick-1-se { + background-position: -48px -48px; +} + +.ui-icon-arrowthick-1-s { + background-position: -64px -48px; +} + +.ui-icon-arrowthick-1-sw { + background-position: -80px -48px; +} + +.ui-icon-arrowthick-1-w { + background-position: -96px -48px; +} + +.ui-icon-arrowthick-1-nw { + background-position: -112px -48px; +} + +.ui-icon-arrowthick-2-n-s { + background-position: -128px -48px; +} + +.ui-icon-arrowthick-2-ne-sw { + background-position: -144px -48px; +} + +.ui-icon-arrowthick-2-e-w { + background-position: -160px -48px; +} + +.ui-icon-arrowthick-2-se-nw { + background-position: -176px -48px; +} + +.ui-icon-arrowthickstop-1-n { + background-position: -192px -48px; +} + +.ui-icon-arrowthickstop-1-e { + background-position: -208px -48px; +} + +.ui-icon-arrowthickstop-1-s { + background-position: -224px -48px; +} + +.ui-icon-arrowthickstop-1-w { + background-position: -240px -48px; +} + +.ui-icon-arrowreturnthick-1-w { + background-position: 0 -64px; +} + +.ui-icon-arrowreturnthick-1-n { + background-position: -16px -64px; +} + +.ui-icon-arrowreturnthick-1-e { + background-position: -32px -64px; +} + +.ui-icon-arrowreturnthick-1-s { + background-position: -48px -64px; +} + +.ui-icon-arrowreturn-1-w { + background-position: -64px -64px; +} + +.ui-icon-arrowreturn-1-n { + background-position: -80px -64px; +} + +.ui-icon-arrowreturn-1-e { + background-position: -96px -64px; +} + +.ui-icon-arrowreturn-1-s { + background-position: -112px -64px; +} + +.ui-icon-arrowrefresh-1-w { + background-position: -128px -64px; +} + +.ui-icon-arrowrefresh-1-n { + background-position: -144px -64px; +} + +.ui-icon-arrowrefresh-1-e { + background-position: -160px -64px; +} + +.ui-icon-arrowrefresh-1-s { + background-position: -176px -64px; +} + +.ui-icon-arrow-4 { + background-position: 0 -80px; +} + +.ui-icon-arrow-4-diag { + background-position: -16px -80px; +} + +.ui-icon-extlink { + background-position: -32px -80px; +} + +.ui-icon-newwin { + background-position: -48px -80px; +} + +.ui-icon-refresh { + background-position: -64px -80px; +} + +.ui-icon-shuffle { + background-position: -80px -80px; +} + +.ui-icon-transfer-e-w { + background-position: -96px -80px; +} + +.ui-icon-transferthick-e-w { + background-position: -112px -80px; +} + +.ui-icon-folder-collapsed { + background-position: 0 -96px; +} + +.ui-icon-folder-open { + background-position: -16px -96px; +} + +.ui-icon-document { + background-position: -32px -96px; +} + +.ui-icon-document-b { + background-position: -48px -96px; +} + +.ui-icon-note { + background-position: -64px -96px; +} + +.ui-icon-mail-closed { + background-position: -80px -96px; +} + +.ui-icon-mail-open { + background-position: -96px -96px; +} + +.ui-icon-suitcase { + background-position: -112px -96px; +} + +.ui-icon-comment { + background-position: -128px -96px; +} + +.ui-icon-person { + background-position: -144px -96px; +} + +.ui-icon-print { + background-position: -160px -96px; +} + +.ui-icon-trash { + background-position: -176px -96px; +} + +.ui-icon-locked { + background-position: -192px -96px; +} + +.ui-icon-unlocked { + background-position: -208px -96px; +} + +.ui-icon-bookmark { + background-position: -224px -96px; +} + +.ui-icon-tag { + background-position: -240px -96px; +} + +.ui-icon-home { + background-position: 0 -112px; +} + +.ui-icon-flag { + background-position: -16px -112px; +} + +.ui-icon-calendar { + background-position: -32px -112px; +} + +.ui-icon-cart { + background-position: -48px -112px; +} + +.ui-icon-pencil { + background-position: -64px -112px; +} + +.ui-icon-clock { + background-position: -80px -112px; +} + +.ui-icon-disk { + background-position: -96px -112px; +} + +.ui-icon-calculator { + background-position: -112px -112px; +} + +.ui-icon-zoomin { + background-position: -128px -112px; +} + +.ui-icon-zoomout { + background-position: -144px -112px; +} + +.ui-icon-search { + background-position: -160px -112px; +} + +.ui-icon-wrench { + background-position: -176px -112px; +} + +.ui-icon-gear { + background-position: -192px -112px; +} + +.ui-icon-heart { + background-position: -208px -112px; +} + +.ui-icon-star { + background-position: -224px -112px; +} + +.ui-icon-link { + background-position: -240px -112px; +} + +.ui-icon-cancel { + background-position: 0 -128px; +} + +.ui-icon-plus { + background-position: -16px -128px; +} + +.ui-icon-plusthick { + background-position: -32px -128px; +} + +.ui-icon-minus { + background-position: -48px -128px; +} + +.ui-icon-minusthick { + background-position: -64px -128px; +} + +.ui-icon-close { + background-position: -80px -128px; +} + +.ui-icon-closethick { + background-position: -96px -128px; +} + +.ui-icon-key { + background-position: -112px -128px; +} + +.ui-icon-lightbulb { + background-position: -128px -128px; +} + +.ui-icon-scissors { + background-position: -144px -128px; +} + +.ui-icon-clipboard { + background-position: -160px -128px; +} + +.ui-icon-copy { + background-position: -176px -128px; +} + +.ui-icon-contact { + background-position: -192px -128px; +} + +.ui-icon-image { + background-position: -208px -128px; +} + +.ui-icon-video { + background-position: -224px -128px; +} + +.ui-icon-script { + background-position: -240px -128px; +} + +.ui-icon-alert { + background-position: 0 -144px; +} + +.ui-icon-info { + background-position: -16px -144px; +} + +.ui-icon-notice { + background-position: -32px -144px; +} + +.ui-icon-help { + background-position: -48px -144px; +} + +.ui-icon-check { + background-position: -64px -144px; +} + +.ui-icon-bullet { + background-position: -80px -144px; +} + +.ui-icon-radio-off { + background-position: -96px -144px; +} + +.ui-icon-radio-on { + background-position: -112px -144px; +} + +.ui-icon-pin-w { + background-position: -128px -144px; +} + +.ui-icon-pin-s { + background-position: -144px -144px; +} + +.ui-icon-play { + background-position: 0 -160px; +} + +.ui-icon-pause { + background-position: -16px -160px; +} + +.ui-icon-seek-next { + background-position: -32px -160px; +} + +.ui-icon-seek-prev { + background-position: -48px -160px; +} + +.ui-icon-seek-end { + background-position: -64px -160px; +} + +.ui-icon-seek-start { + background-position: -80px -160px; +} + +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { + background-position: -80px -160px; +} + +.ui-icon-stop { + background-position: -96px -160px; +} + +.ui-icon-eject { + background-position: -112px -160px; +} + +.ui-icon-volume-off { + background-position: -128px -160px; +} + +.ui-icon-volume-on { + background-position: -144px -160px; +} + +.ui-icon-power { + background-position: 0 -176px; +} + +.ui-icon-signal-diag { + background-position: -16px -176px; +} + +.ui-icon-signal { + background-position: -32px -176px; +} + +.ui-icon-battery-0 { + background-position: -48px -176px; +} + +.ui-icon-battery-1 { + background-position: -64px -176px; +} + +.ui-icon-battery-2 { + background-position: -80px -176px; +} + +.ui-icon-battery-3 { + background-position: -96px -176px; +} + +.ui-icon-circle-plus { + background-position: 0 -192px; +} + +.ui-icon-circle-minus { + background-position: -16px -192px; +} + +.ui-icon-circle-close { + background-position: -32px -192px; +} + +.ui-icon-circle-triangle-e { + background-position: -48px -192px; +} + +.ui-icon-circle-triangle-s { + background-position: -64px -192px; +} + +.ui-icon-circle-triangle-w { + background-position: -80px -192px; +} + +.ui-icon-circle-triangle-n { + background-position: -96px -192px; +} + +.ui-icon-circle-arrow-e { + background-position: -112px -192px; +} +.ui-icon-circle-arrow-s { + background-position: -128px -192px; +} -.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555; text-decoration: none; } -.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { - background-position: 0 -15px; - color: #333; - text-decoration: none; - } +.ui-icon-circle-arrow-w { + background-position: -144px -192px; +} -.ui-state-hover a, .ui-state-hover a:hover, .ui-state-hover a:link, .ui-state-hover a:visited { color: #212121; text-decoration: none; } -.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa; font-weight: normal; color: #212121; } -.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121; text-decoration: none; } -.ui-widget :active { outline: none; } +.ui-icon-circle-arrow-n { + background-position: -160px -192px; +} -/* Interaction Cues -----------------------------------*/ +.ui-icon-circle-zoomin { + background-position: -176px -192px; +} +.ui-icon-circle-zoomout { + background-position: -192px -192px; +} -.ui-state-highlight p, .ui-state-error p, .ui-state-default p{ - font-size: 13px; - font-weight: normal; - line-height: 18px; - margin:7px 15px; +.ui-icon-circle-check { + background-position: -208px -192px; } -.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight { +.ui-icon-circlesmall-plus { + background-position: 0 -208px; +} - position: relative; - margin-bottom: 18px; - color: #404040; - background-color: #eedc94; - background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94)); - background-image: -moz-linear-gradient(top, #fceec1, #eedc94); - background-image: -ms-linear-gradient(top, #fceec1, #eedc94); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94)); - background-image: -webkit-linear-gradient(top, #fceec1, #eedc94); - background-image: -o-linear-gradient(top, #fceec1, #eedc94); - background-image: linear-gradient(top, #fceec1, #eedc94); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0); - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - border-color: #eedc94 #eedc94 #e4c652; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - border-width: 1px; - border-style: solid; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); +.ui-icon-circlesmall-minus { + background-position: -16px -208px; +} +.ui-icon-circlesmall-close { + background-position: -32px -208px; +} +.ui-icon-squaresmall-plus { + background-position: -48px -208px; } -.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; } -.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error { +.ui-icon-squaresmall-minus { + background-position: -64px -208px; +} - position: relative; - margin-bottom: 18px; - color: #ffffff; - border-width: 1px; - border-style: solid; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); - background-color: #c43c35; - background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35)); - background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35)); - background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); - background-image: linear-gradient(top, #ee5f5b, #c43c35); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0); - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - border-color: #c43c35 #c43c35 #882a25; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); +.ui-icon-squaresmall-close { + background-position: -80px -208px; +} +.ui-icon-grip-dotted-vertical { + background-position: 0 -224px; +} +.ui-icon-grip-dotted-horizontal { + background-position: -16px -224px; } -.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a; } -.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a; } -.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } -.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } -.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } -.ui-state-disabled .ui-icon { filter:Alpha(Opacity=35); } /* For IE8 - See #6059 */ +.ui-icon-grip-solid-vertical { + background-position: -32px -224px; +} -/* Icons -----------------------------------*/ +.ui-icon-grip-solid-horizontal { + background-position: -48px -224px; +} -/* states and images */ -.ui-icon { width: 16px; height: 16px; background-image: image-url("ui-icons_222222_256x240.png"); } -.ui-widget-content .ui-icon {background-image: image-url("ui-icons_222222_256x240.png"); } -.ui-widget-header .ui-icon {background-image: image-url("ui-icons_222222_256x240.png"); } -.ui-state-default .ui-icon { background-image: image-url("ui-icons_888888_256x240.png"); } -.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: image-url("ui-icons_454545_256x240.png"); } -.ui-state-active .ui-icon {background-image: image-url("ui-icons_454545_256x240.png"); } -.ui-state-highlight .ui-icon {background-image: image-url("ui-icons_2e83ff_256x240.png"); } -.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: image-url("ui-icons_f6cf3b_256x240.png"); } +.ui-icon-gripsmall-diagonal-se { + background-position: -64px -224px; +} -/* positioning */ -.ui-icon-carat-1-n { background-position: 0 0; } -.ui-icon-carat-1-ne { background-position: -16px 0; } -.ui-icon-carat-1-e { background-position: -32px 0; } -.ui-icon-carat-1-se { background-position: -48px 0; } -.ui-icon-carat-1-s { background-position: -64px 0; } -.ui-icon-carat-1-sw { background-position: -80px 0; } -.ui-icon-carat-1-w { background-position: -96px 0; } -.ui-icon-carat-1-nw { background-position: -112px 0; } -.ui-icon-carat-2-n-s { background-position: -128px 0; } -.ui-icon-carat-2-e-w { background-position: -144px 0; } -.ui-icon-triangle-1-n { background-position: 0 -16px; } -.ui-icon-triangle-1-ne { background-position: -16px -16px; } -.ui-icon-triangle-1-e { background-position: -32px -16px; } -.ui-icon-triangle-1-se { background-position: -48px -16px; } -.ui-icon-triangle-1-s { background-position: -64px -16px; } -.ui-icon-triangle-1-sw { background-position: -80px -16px; } -.ui-icon-triangle-1-w { background-position: -96px -16px; } -.ui-icon-triangle-1-nw { background-position: -112px -16px; } -.ui-icon-triangle-2-n-s { background-position: -128px -16px; } -.ui-icon-triangle-2-e-w { background-position: -144px -16px; } -.ui-icon-arrow-1-n { background-position: 0 -32px; } -.ui-icon-arrow-1-ne { background-position: -16px -32px; } -.ui-icon-arrow-1-e { background-position: -32px -32px; } -.ui-icon-arrow-1-se { background-position: -48px -32px; } -.ui-icon-arrow-1-s { background-position: -64px -32px; } -.ui-icon-arrow-1-sw { background-position: -80px -32px; } -.ui-icon-arrow-1-w { background-position: -96px -32px; } -.ui-icon-arrow-1-nw { background-position: -112px -32px; } -.ui-icon-arrow-2-n-s { background-position: -128px -32px; } -.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } -.ui-icon-arrow-2-e-w { background-position: -160px -32px; } -.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } -.ui-icon-arrowstop-1-n { background-position: -192px -32px; } -.ui-icon-arrowstop-1-e { background-position: -208px -32px; } -.ui-icon-arrowstop-1-s { background-position: -224px -32px; } -.ui-icon-arrowstop-1-w { background-position: -240px -32px; } -.ui-icon-arrowthick-1-n { background-position: 0 -48px; } -.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } -.ui-icon-arrowthick-1-e { background-position: -32px -48px; } -.ui-icon-arrowthick-1-se { background-position: -48px -48px; } -.ui-icon-arrowthick-1-s { background-position: -64px -48px; } -.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } -.ui-icon-arrowthick-1-w { background-position: -96px -48px; } -.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } -.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } -.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } -.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } -.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } -.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } -.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } -.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } -.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } -.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } -.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } -.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } -.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } -.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } -.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } -.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } -.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } -.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } -.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } -.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } -.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } -.ui-icon-arrow-4 { background-position: 0 -80px; } -.ui-icon-arrow-4-diag { background-position: -16px -80px; } -.ui-icon-extlink { background-position: -32px -80px; } -.ui-icon-newwin { background-position: -48px -80px; } -.ui-icon-refresh { background-position: -64px -80px; } -.ui-icon-shuffle { background-position: -80px -80px; } -.ui-icon-transfer-e-w { background-position: -96px -80px; } -.ui-icon-transferthick-e-w { background-position: -112px -80px; } -.ui-icon-folder-collapsed { background-position: 0 -96px; } -.ui-icon-folder-open { background-position: -16px -96px; } -.ui-icon-document { background-position: -32px -96px; } -.ui-icon-document-b { background-position: -48px -96px; } -.ui-icon-note { background-position: -64px -96px; } -.ui-icon-mail-closed { background-position: -80px -96px; } -.ui-icon-mail-open { background-position: -96px -96px; } -.ui-icon-suitcase { background-position: -112px -96px; } -.ui-icon-comment { background-position: -128px -96px; } -.ui-icon-person { background-position: -144px -96px; } -.ui-icon-print { background-position: -160px -96px; } -.ui-icon-trash { background-position: -176px -96px; } -.ui-icon-locked { background-position: -192px -96px; } -.ui-icon-unlocked { background-position: -208px -96px; } -.ui-icon-bookmark { background-position: -224px -96px; } -.ui-icon-tag { background-position: -240px -96px; } -.ui-icon-home { background-position: 0 -112px; } -.ui-icon-flag { background-position: -16px -112px; } -.ui-icon-calendar { background-position: -32px -112px; } -.ui-icon-cart { background-position: -48px -112px; } -.ui-icon-pencil { background-position: -64px -112px; } -.ui-icon-clock { background-position: -80px -112px; } -.ui-icon-disk { background-position: -96px -112px; } -.ui-icon-calculator { background-position: -112px -112px; } -.ui-icon-zoomin { background-position: -128px -112px; } -.ui-icon-zoomout { background-position: -144px -112px; } -.ui-icon-search { background-position: -160px -112px; } -.ui-icon-wrench { background-position: -176px -112px; } -.ui-icon-gear { background-position: -192px -112px; } -.ui-icon-heart { background-position: -208px -112px; } -.ui-icon-star { background-position: -224px -112px; } -.ui-icon-link { background-position: -240px -112px; } -.ui-icon-cancel { background-position: 0 -128px; } -.ui-icon-plus { background-position: -16px -128px; } -.ui-icon-plusthick { background-position: -32px -128px; } -.ui-icon-minus { background-position: -48px -128px; } -.ui-icon-minusthick { background-position: -64px -128px; } -.ui-icon-close { background-position: -80px -128px; } -.ui-icon-closethick { background-position: -96px -128px; } -.ui-icon-key { background-position: -112px -128px; } -.ui-icon-lightbulb { background-position: -128px -128px; } -.ui-icon-scissors { background-position: -144px -128px; } -.ui-icon-clipboard { background-position: -160px -128px; } -.ui-icon-copy { background-position: -176px -128px; } -.ui-icon-contact { background-position: -192px -128px; } -.ui-icon-image { background-position: -208px -128px; } -.ui-icon-video { background-position: -224px -128px; } -.ui-icon-script { background-position: -240px -128px; } -.ui-icon-alert { background-position: 0 -144px; } -.ui-icon-info { background-position: -16px -144px; } -.ui-icon-notice { background-position: -32px -144px; } -.ui-icon-help { background-position: -48px -144px; } -.ui-icon-check { background-position: -64px -144px; } -.ui-icon-bullet { background-position: -80px -144px; } -.ui-icon-radio-off { background-position: -96px -144px; } -.ui-icon-radio-on { background-position: -112px -144px; } -.ui-icon-pin-w { background-position: -128px -144px; } -.ui-icon-pin-s { background-position: -144px -144px; } -.ui-icon-play { background-position: 0 -160px; } -.ui-icon-pause { background-position: -16px -160px; } -.ui-icon-seek-next { background-position: -32px -160px; } -.ui-icon-seek-prev { background-position: -48px -160px; } -.ui-icon-seek-end { background-position: -64px -160px; } -.ui-icon-seek-start { background-position: -80px -160px; } -/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ -.ui-icon-seek-first { background-position: -80px -160px; } -.ui-icon-stop { background-position: -96px -160px; } -.ui-icon-eject { background-position: -112px -160px; } -.ui-icon-volume-off { background-position: -128px -160px; } -.ui-icon-volume-on { background-position: -144px -160px; } -.ui-icon-power { background-position: 0 -176px; } -.ui-icon-signal-diag { background-position: -16px -176px; } -.ui-icon-signal { background-position: -32px -176px; } -.ui-icon-battery-0 { background-position: -48px -176px; } -.ui-icon-battery-1 { background-position: -64px -176px; } -.ui-icon-battery-2 { background-position: -80px -176px; } -.ui-icon-battery-3 { background-position: -96px -176px; } -.ui-icon-circle-plus { background-position: 0 -192px; } -.ui-icon-circle-minus { background-position: -16px -192px; } -.ui-icon-circle-close { background-position: -32px -192px; } -.ui-icon-circle-triangle-e { background-position: -48px -192px; } -.ui-icon-circle-triangle-s { background-position: -64px -192px; } -.ui-icon-circle-triangle-w { background-position: -80px -192px; } -.ui-icon-circle-triangle-n { background-position: -96px -192px; } -.ui-icon-circle-arrow-e { background-position: -112px -192px; } -.ui-icon-circle-arrow-s { background-position: -128px -192px; } -.ui-icon-circle-arrow-w { background-position: -144px -192px; } -.ui-icon-circle-arrow-n { background-position: -160px -192px; } -.ui-icon-circle-zoomin { background-position: -176px -192px; } -.ui-icon-circle-zoomout { background-position: -192px -192px; } -.ui-icon-circle-check { background-position: -208px -192px; } -.ui-icon-circlesmall-plus { background-position: 0 -208px; } -.ui-icon-circlesmall-minus { background-position: -16px -208px; } -.ui-icon-circlesmall-close { background-position: -32px -208px; } -.ui-icon-squaresmall-plus { background-position: -48px -208px; } -.ui-icon-squaresmall-minus { background-position: -64px -208px; } -.ui-icon-squaresmall-close { background-position: -80px -208px; } -.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } -.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } -.ui-icon-grip-solid-vertical { background-position: -32px -224px; } -.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } -.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } -.ui-icon-grip-diagonal-se { background-position: -80px -224px; } +.ui-icon-grip-diagonal-se { + background-position: -80px -224px; +} /* Misc visuals @@ -553,6 +1221,7 @@ -khtml-border-top-left-radius: 4px; border-top-left-radius: 4px; } + .ui-corner-all, .ui-corner-top, .ui-corner-right, @@ -562,6 +1231,7 @@ -khtml-border-top-right-radius: 4px; border-top-right-radius: 4px; } + .ui-corner-all, .ui-corner-bottom, .ui-corner-left, @@ -571,6 +1241,7 @@ -khtml-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; } + .ui-corner-all, .ui-corner-bottom, .ui-corner-right, @@ -587,14 +1258,15 @@ .ui-widget-overlay { background: #aaaaaa image-url("ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x; opacity: .30; - filter:Alpha(Opacity=30); + filter: Alpha(Opacity=30); } .ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa image-url("ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x; - opacity: .30;filter:Alpha(Opacity=30); + opacity: .30; + filter: Alpha(Opacity=30); -moz-border-radius: 8px; -khtml-border-radius: 8px; -webkit-border-radius: 8px; @@ -825,11 +1497,46 @@ button.ui-button::-moz-focus-inner { border: 0; padding: 0; } * http://docs.jquery.com/UI/Menu#theming */ -.ui-menu { list-style:none; padding: 2px; margin: 0; display:block; float:left; outline: none; } -.ui-menu .ui-menu { margin-top: -3px; position: absolute; } -.ui-menu .ui-menu-item { margin: 0; padding: 0; zoom: 1;float: left;clear: left; width: 100%; } -.ui-menu .ui-menu-divider { margin: 5px -2px 5px -2px; height: 0; font-size: 0; line-height: 0; border-width: 1px 0 0 0; } -.ui-menu .ui-menu-item a { text-decoration: none; display: block; padding: 2px .4em; line-height: 1.5; zoom: 1; font-weight: normal; } +.ui-menu { + list-style: none; + padding: 2px; + margin: 0; + display: block; + float: left; + outline: none; +} + +.ui-menu .ui-menu { + margin-top: -3px; + position: absolute; +} + +.ui-menu .ui-menu-item { + margin: 0; + padding: 0; + zoom: 1; + float: left; + clear: left; + width: 100%; +} + +.ui-menu .ui-menu-divider { + margin: 5px -2px 5px -2px; + height: 0; + font-size: 0; + line-height: 0; + border-width: 1px 0 0 0; +} + +.ui-menu .ui-menu-item a { + text-decoration: none; + display: block; + padding: 2px .4em; + line-height: 1.5; + zoom: 1; + font-weight: normal; +} + .ui-menu .ui-menu-item a.ui-state-focus, .ui-menu .ui-menu-item a.ui-state-active { font-weight: normal; @@ -839,15 +1546,15 @@ button.ui-button::-moz-focus-inner { border: 0; padding: 0; } background-color: #238bca; background-repeat: repeat-x; background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#238bca)); - background-image: -moz-linear-gradient(top, #049cdb, #238bca); - background-image: -ms-linear-gradient(top, #049cdb, #238bca); + background-image: -moz-linear-gradient(to bottom, #049cdb, #238bca); + background-image: -ms-linear-gradient(to bottom, #049cdb, #238bca); background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #238bca)); - background-image: -webkit-linear-gradient(top, #049cdb, #238bca); - background-image: -o-linear-gradient(top, #049cdb, #238bca); - background-image: linear-gradient(top, #049cdb, #238bca); + background-image: -webkit-linear-gradient(to bottom, #049cdb, #238bca); + background-image: -o-linear-gradient(to bottom, #049cdb, #238bca); + background-image: linear-gradient(to bottom, #049cdb, #238bca); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#238bca', GradientType=0); text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - border-color: #238bca #238bca #003f81; + border-color: #238bca #238bca #003f81; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } @@ -855,17 +1562,40 @@ button.ui-button::-moz-focus-inner { border: 0; padding: 0; } padding: 1px .4em; } -.ui-menu .ui-state-disabled { font-weight: normal; margin: .4em 0 .2em; line-height: 1.5; } -.ui-menu .ui-state-disabled a { cursor: default; } +.ui-menu .ui-state-disabled { + font-weight: normal; + margin: .4em 0 .2em; + line-height: 1.5; +} + +.ui-menu .ui-state-disabled a { + cursor: default; +} + +.ui-menu-icons { + position: relative; +} -.ui-menu-icons { position: relative; } -.ui-menu-icons .ui-menu-item a { position: relative; padding-left: 2em; } +.ui-menu-icons .ui-menu-item a { + position: relative; + padding-left: 2em; +} -.ui-menu .ui-icon { position: absolute; top: .2em; left: .2em; } +.ui-menu .ui-icon { + position: absolute; + top: .2em; + left: .2em; +} -.ui-menu .ui-menu-icon { position: static; float: right; } +.ui-menu .ui-menu-icon { + position: static; + float: right; +} -.ui-menu { width: 200px; margin-bottom: 2em; } +.ui-menu { + width: 200px; + margin-bottom: 2em; +} /* * jQuery UI spinner 1.10.0 @@ -1170,112 +1900,294 @@ body .ui-tooltip { border-width:2px; } * * http://jqueryui.com/datepicker/ */ -.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; } -.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; border:0px; font-weight: bold; width: 100%; padding: 4px 0; background-color: #f5f5f5; color: #808080; } -.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; } +.ui-datepicker { + width: 17em; + padding: .2em .2em 0; + display: none; +} + +.ui-datepicker .ui-datepicker-header { + position: relative; + padding: .2em 0; + border: 0px; + font-weight: bold; + width: 100%; + padding: 4px 0; + background-color: #f5f5f5; + color: #808080; +} + +.ui-datepicker .ui-datepicker-prev, +.ui-datepicker .ui-datepicker-next { + position: absolute; + top: 2px; + width: 1.8em; + height: 1.8em; +} + +.ui-datepicker .ui-datepicker-prev-hover, +.ui-datepicker .ui-datepicker-next-hover { + /*top: 1px;*/ +} + +.ui-datepicker .ui-datepicker-prev { + left: 2px; +} + +.ui-datepicker .ui-datepicker-next { + right: 2px; +} + +.ui-datepicker .ui-datepicker-prev-hover { + /*left:1px;*/ +} + +.ui-datepicker .ui-datepicker-next-hover { + /*right:1px;*/ +} + +.ui-datepicker .ui-datepicker-prev span, +.ui-datepicker .ui-datepicker-next span { + display: block; + position: absolute; + left: 50%; + margin-left: -8px; + top: 50%; + margin-top: -8px; +} + +.ui-datepicker .ui-datepicker-title { + margin: 0 2.3em; + line-height: 1.8em; + text-align: center; +} -.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { /*top: 1px;*/ } -.ui-datepicker .ui-datepicker-prev { left:2px; } -.ui-datepicker .ui-datepicker-next { right:2px; } +.ui-datepicker .ui-datepicker-title select { + font-size: 1em; + margin: 1px 0; +} -.ui-datepicker .ui-datepicker-prev-hover { /*left:1px;*/ } -.ui-datepicker .ui-datepicker-next-hover { /*right:1px;*/ } +.ui-datepicker select.ui-datepicker-month-year { + width: 100%; +} -.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; } -.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; } -.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; } -.ui-datepicker select.ui-datepicker-month-year {width: 100%;} .ui-datepicker select.ui-datepicker-month, -.ui-datepicker select.ui-datepicker-year { width: 49%;} -.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; } -.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; } -.ui-datepicker td { border: 0; padding: 1px; } -.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; } -.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; } -.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } -.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; } +.ui-datepicker select.ui-datepicker-year { + width: 49%; +} + +.ui-datepicker table { + width: 100%; + font-size: .9em; + border-collapse: collapse; + margin: 0 0 .4em; +} + +.ui-datepicker th { + padding: .7em .3em; + text-align: center; + font-weight: bold; + border: 0; +} + +.ui-datepicker td { + border: 0; + padding: 1px; +} + +.ui-datepicker td span, +.ui-datepicker td a { + display: block; + padding: .2em; + text-align: right; + text-decoration: none; +} + +.ui-datepicker .ui-datepicker-buttonpane { + background-image: none; + margin: .7em 0 0 0; + padding: 0 .2em; + border-left: 0; + border-right: 0; + border-bottom: 0; +} + +.ui-datepicker .ui-datepicker-buttonpane button { + float: right; + margin: .5em .2em .4em; + cursor: pointer; + padding: .2em .6em .3em .6em; + width: auto; + overflow: visible; +} + +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { + float: left; +} /* with multiple calendars */ -.ui-datepicker.ui-datepicker-multi { width:auto; } -.ui-datepicker-multi .ui-datepicker-group { float:left; } -.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; } -.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; } -.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; } -.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; } -.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; } -.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; } -.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; } -.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; } +.ui-datepicker.ui-datepicker-multi { + width: auto; +} + +.ui-datepicker-multi .ui-datepicker-group { + float: left; +} + +.ui-datepicker-multi .ui-datepicker-group table { + width: 95%; + margin: 0 auto .4em; +} + +.ui-datepicker-multi-2 .ui-datepicker-group { + width: 50%; +} + +.ui-datepicker-multi-3 .ui-datepicker-group { + width: 33.3%; +} + +.ui-datepicker-multi-4 .ui-datepicker-group { + width: 25%; +} + +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { + border-left-width: 0; +} + +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { + border-left-width: 0; +} + +.ui-datepicker-multi .ui-datepicker-buttonpane { + clear: left; +} + +.ui-datepicker-row-break { + clear: both; + width: 100%; + font-size: 0em; +} /* RTL support */ -.ui-datepicker-rtl { direction: rtl; } -.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; } -.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; } -.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; } -.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; } -.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; } -.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; } -.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; } -.ui-datepicker-rtl .ui-datepicker-group { float:right; } -.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; } -.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; } +.ui-datepicker-rtl { + direction: rtl; +} + +.ui-datepicker-rtl .ui-datepicker-prev { + right: 2px; + left: auto; +} + +.ui-datepicker-rtl .ui-datepicker-next { + left: 2px; + right: auto; +} + +.ui-datepicker-rtl .ui-datepicker-prev:hover { + right: 1px; + left: auto; +} + +.ui-datepicker-rtl .ui-datepicker-next:hover { + left: 1px; + right: auto; +} + +.ui-datepicker-rtl .ui-datepicker-buttonpane { + clear: right; +} + +.ui-datepicker-rtl .ui-datepicker-buttonpane button { + float: left; +} + +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { + float: right; +} + +.ui-datepicker-rtl .ui-datepicker-group { + float: right; +} + +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { + border-right-width: 0; + border-left-width: 1px; +} + +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { + border-right-width: 0; + border-left-width: 1px; +} /* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ .ui-datepicker-cover { - display: none; /*sorry for IE5*/ - display/**/: block; /*sorry for IE5*/ - position: absolute; /*must have*/ - z-index: -1; /*must have*/ - filter: mask(); /*must have*/ - top: -4px; /*must have*/ - left: -4px; /*must have*/ - width: 200px; /*must have*/ - height: 200px; /*must have*/ -} - -.ui-datepicker th{ + display: none; + /*sorry for IE5*/ + display + /**/ + : block; + /*sorry for IE5*/ + position: absolute; + /*must have*/ + z-index: -1; + /*must have*/ + filter: mask(); + /*must have*/ + top: -4px; + /*must have*/ + left: -4px; + /*must have*/ + width: 200px; + /*must have*/ + height: 200px; + /*must have*/ +} + +.ui-datepicker th { font-weight: bold; color: gray; } -.ui-datepicker-today a:hover{ +.ui-datepicker-today a:hover { background-color: #808080; color: #ffffff; } -.ui-datepicker-today a{ + +.ui-datepicker-today a { background-color: #BFBFBF; cursor: pointer; padding: 0 4px; - margin-bottom:0px; + margin-bottom: 0px; } -.ui-datepicker td a{ - margin-bottom:0px; - border:0px; +.ui-datepicker td a { + margin-bottom: 0px; + border: 0px; } -.ui-datepicker td:hover{ - color: #ffffff; +.ui-datepicker td:hover { + color: #ffffff; } .ui-datepicker td .ui-state-default { - border:0px; - background:none; - margin-bottom:0px; - padding:5px; - color:gray; + border: 0px; + background: none; + margin-bottom: 0px; + padding: 5px; + color: gray; text-align: center; - filter:none; + filter: none; } -.ui-datepicker td .ui-state-active{ - background:#BFBFBF; - margin-bottom:0px; - font-size:normal; +.ui-datepicker td .ui-state-active { + background: #BFBFBF; + margin-bottom: 0px; + font-size: normal; text-shadow: 0px; color: #ffffff; -webkit-border-radius: 4px; @@ -1284,25 +2196,25 @@ body .ui-tooltip { border-width:2px; } } .ui-datepicker td .ui-state-hover { - color: #ffffff; - background: #238bca; - background-color: #238bca; - background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#238bca)); - background-image: -moz-linear-gradient(top, #049cdb, #238bca); - background-image: -ms-linear-gradient(top, #049cdb, #238bca); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #238bca)); - background-image: -webkit-linear-gradient(top, #049cdb, #238bca); - background-image: -o-linear-gradient(top, #049cdb, #238bca); - background-image: linear-gradient(top, #049cdb, #238bca); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#238bca', GradientType=0); - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - border-color: #238bca #238bca #003f81; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -khtml-border-radius: 4px; - border-radius: 4px; + color: #ffffff; + background: #238bca; + background-color: #238bca; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#238bca)); + background-image: -moz-linear-gradient(to bottom, #049cdb, #238bca); + background-image: -ms-linear-gradient(to bottom, #049cdb, #238bca); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #238bca)); + background-image: -webkit-linear-gradient(to bottom, #049cdb, #238bca); + background-image: -o-linear-gradient(to bottom, #049cdb, #238bca); + background-image: linear-gradient(to bottom, #049cdb, #238bca); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#238bca', GradientType=0); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + border-color: #238bca #238bca #003f81; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -khtml-border-radius: 4px; + border-radius: 4px; } /*