diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index c80dac1e1..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "parserOptions": { - "ecmaVersion": 2017, - "sourceType": "module", - "ecmaFeatures": { - } - }, - "rules": { - "semi": "error" - } -} \ No newline at end of file diff --git a/.github/actions/changed_files/action.yml b/.github/actions/changed_files/action.yml new file mode 100644 index 000000000..0af0b6e91 --- /dev/null +++ b/.github/actions/changed_files/action.yml @@ -0,0 +1,42 @@ +# Why is this file in a subdirectory? Because GitHub Actions requires it to be :( +# see: https://github.com/orgs/community/discussions/26245#discussioncomment-5962450 +name: "Get changed files" +description: "Checks out the code and returns the filenames of files that have changed in the pull request" + +inputs: + file-extensions: + # for example: "\.rb$" or something like "\.js$|\.js.erb$" + description: "Regex expressions for grep to filter for specific files" + required: true + +outputs: + changed-files: + description: "A space-separated list of the files that have changed in the pull request" + value: ${{ steps.get-changed-files.outputs.files }} + +runs: + using: "composite" + steps: + # This has to be done in the main workflow, not in the action, because + # otherwise this reusable action is not available in the workflow. + # - name: "Checkout code (on a PR branch)" + # uses: actions/checkout@v4 + # with: + # fetch-depth: 2 # to also fetch parent of PR + + # Adapted from this great comment [1]. Git diff adapted from [2]. + # "|| test $? = 1;" is used to ignore the exit code of grep when no files + # are found matching the pattern. For the "three dots" ... syntax, see [3]. + # + # Resources: + # number [1] being most important + # [1] https://github.com/actions/checkout/issues/520#issuecomment-1167205721 + # [2] https://robertfaldo.medium.com/commands-to-run-rubocop-and-specs-you-changed-in-your-branch-e6d2f2e4110b + # [3] https://community.atlassian.com/t5/Bitbucket-questions/Git-diff-show-different-files-than-PR-Pull-Request/qaq-p/2331786 + - name: Get changed files + shell: bash + id: get-changed-files + run: | + files_pretty=$(git diff --name-only --diff-filter=ACMR -r HEAD^1...HEAD | egrep '${{inputs.file-extensions}}' || test $? = 1;) + printf "🎴 Changed files: \n$files_pretty" + echo "files=$(echo ${files_pretty} | xargs)" >> $GITHUB_OUTPUT diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index ff55d914e..e40132f75 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,87 +1,75 @@ -on: [push] +name: Linting + +# Trigger each time HEAD branch is updated in a pull request +# see https://github.com/orgs/community/discussions/26366 +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] jobs: + rubocop: + name: RuboCop (Ruby) runs-on: ubuntu-latest - name: A job to check rubocop linter errors steps: - - uses: actions/checkout@v2 + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 2 # to also fetch parent of PR (used to get changed files) + + - name: Get changed files + id: rb-changed + uses: ./.github/actions/changed_files/ + with: + file-extensions: \.rb$ + - name: Set up Ruby 3 + if: ${{ steps.rb-changed.outputs.changed-files != ''}} uses: ruby/setup-ruby@v1 with: ruby-version: 3.1.4 - - name: Install gems # usual step to install the gems. + bundler-cache: true + + - name: Run RuboCop + if: ${{ steps.rb-changed.outputs.changed-files != ''}} run: | - bin/bundle config path vendor/bundle - bin/bundle config set without 'default doc job cable storage ujs test db' - bin/bundle install --jobs 4 --retry 3 - - name: Linter count - id: hello - uses: henrixapp/linter-less-or-equal-action@v1.19 - with: - name: Rubocop - command: bin/bundle exec rubocop app config lib spec - total_regexp: \d+ offenses detected - errors_regexp: \d+ offenses detected - warnings_regexp: \d+ offenses detected - compare_branch: mampf-next - mode: changed - include: .rb + echo "🚨 Running RuboCop version: $(bundle info rubocop | head -1)" + bundle exec rubocop --format github --fail-level 'convention' --force-exclusion -- $CHANGED_FILES + eslint: - runs-on: ubuntu-latest - name: A job to check eslint linter errors - steps: - - uses: actions/checkout@v2 - - name: Linter count - id: hello - uses: henrixapp/linter-less-or-equal-action@v1.19 - with: - name: EsLint - command: npx eslint - total_regexp: \d+ problems - errors_regexp: \d+ errors - warnings_regexp: \d+ warnings - compare_branch: mampf-next - mode: changed - include: .js - coffee: - runs-on: ubuntu-latest - name: A job to check coffee linter errors - steps: - - uses: actions/checkout@v2 - - name: Linter count - id: hello - uses: henrixapp/linter-less-or-equal-action@v1.19 - with: - name: Coffee - command: npx coffeelint - total_regexp: \d+ errors - errors_regexp: \d+ errors - warnings_regexp: \d+ warnings - compare_branch: mampf-next - mode: changed - include: .coffee - # erblint: - # runs-on: ubuntu-latest - # name: A job to check erblint linter errors - # steps: - # - uses: actions/checkout@v2 - # - name: Set up Ruby 2.7 - # uses: ruby/setup-ruby@v1 - # with: - # ruby-version: 2.7 - # - name: Install gems # usual step to install the gems. - # run: | - # bin/bundle config path vendor/bundle - # bin/bundle config set without 'default doc job cable storage ujs test db' - # bin/bundle install --jobs 4 --retry 3 - # - name: Linter count - # id: hello - # uses: henrixapp/linter-less-or-equal-action@v1.1 - # with: - # name: Erblint - # command: bin/bundle exec erblint . - # total_regexp: \d+ error(s) - # errors_regexp: \d+ error(s) - # warnings_regexp: \d+ error(s) - # compare_branch: mampf-next + name: ESLint (JS) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 2 # to also fetch parent of PR (used to get changed files) + + - name: Get changed files + id: js-changed + uses: ./.github/actions/changed_files/ + with: + # .(mjs is only used for eslint.config.mjs as of January 2024) + file-extensions: \.js$|\.mjs$|\.js.erb$ + + - name: Setup Node.js + if: ${{ steps.js-changed.outputs.changed-files != ''}} + uses: actions/setup-node@v4 + with: + node-version: '20' # End of Life (EOL): April 2026 + cache: 'yarn' + + - name: Install dependencies + if: ${{ steps.js-changed.outputs.changed-files != ''}} + run: yarn install + + # with ESLint v9 --ignore-path does not exist anymore + # see [1] for the PR. However, my feeling for this is totally reflected + # by [2]. Hopefully, it will come back in future versions. + # [1] https://github.com/eslint/eslint/pull/16355 + # [2] https://github.com/eslint/eslint/issues/16264#issuecomment-1292858747 + - name: Run ESLint + if: ${{ steps.js-changed.outputs.changed-files != ''}} + run: | + echo "🚨 Running ESLint version: $(yarn run --silent eslint --version)" + yarn run eslint --max-warnings 0 --no-warn-ignored ${{ steps.js-changed.outputs.changed-files }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index ac0fb3280..000000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Tests - -on: - push: - branches: - - main - - mampf-next - - production - - experimental - pull_request: - -jobs: - unit-test-job: - name: Execute unit tests & upload to Codecov - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Pull docker images - run: docker compose pull --ignore-buildable - working-directory: docker/run_cypress_tests - - - name: Use Docker layer caching # https://github.com/jpribyl/action-docker-layer-caching - uses: jpribyl/action-docker-layer-caching@v0.1.1 - continue-on-error: true - - - name: Build docker containers - run: docker compose build - working-directory: docker/run_cypress_tests - - name: Create and migrate DB - run: docker compose run --entrypoint "" mampf sh -c "rake db:create db:migrate db:test:prepare" - working-directory: docker/run_cypress_tests - - name: Reindex sunspot - working-directory: docker/run_cypress_tests - run: | - docker compose run --entrypoint="" mampf sh -c "RAILS_ENV=test rake sunspot:reindex" - - - name: Run unit tests - working-directory: docker/run_cypress_tests - run: docker compose run --entrypoint="" mampf sh -c "RAILS_ENV=test rails spec" - - name: Send test coverage report to Codecov - uses: codecov/codecov-action@v3 - with: - files: ./coverage/coverage.xml - fail_ci_if_error: true - verbose: true - e2e-test-job: - name: Run E2E tests & upload results to Cypress - runs-on: ubuntu-latest - timeout-minutes: 30 - needs: unit-test-job - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Pull docker images - run: docker compose pull --ignore-buildable - working-directory: docker/run_cypress_tests - - - name: Use Docker layer caching # https://github.com/jpribyl/action-docker-layer-caching - uses: jpribyl/action-docker-layer-caching@v0.1.1 - continue-on-error: true - - - name: Build docker containers - run: docker compose build - working-directory: docker/run_cypress_tests - - name: Create and migrate DB - run: docker compose run --entrypoint "" mampf sh -c "rake db:create db:migrate db:test:prepare" - working-directory: docker/run_cypress_tests - - name: Run integration tests - working-directory: docker/run_cypress_tests - env: - # pass the Dashboard record key as an environment variable - CYPRESS_baseUrl: http://mampf:3000 - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - # pass GitHub token to allow accurately detecting a build vs a re-run build - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: docker compose run -e GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} --entrypoint="" cypress_runner sh -c "while ! curl http://mampf:3000 ; do echo waiting for MaMpf to come online at http://mampf:3000; sleep 3; done; cypress run --record --key ${{ secrets.CYPRESS_RECORD_KEY }}" - diff --git a/.rubocop.yml b/.rubocop.yml index d7b548944..c625d63ec 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,341 +1,123 @@ +# SEVERITY LEVELS +# In the GitHub CI/CD, we can only distinguish between "error" and "warning". +# In Rubocop this is much more granular: "info", "refactor", "convention", +# "warning", "error" and "fatal". +# From the docs: "The level is normally 'warning' for Lint and 'convention' for +# all the others, but this can be changed in user configuration." +# However, we don't want to set the severity for each cop individually, so instead +# we set the fatal level as "convention" in the CI/CD, +# i.e. severity: "convention" and up will be treated as an error on GitHub +# and below as warning such that the check will still pass (with a warning). +# +# Overview: +# | RuboCop | GitHub | +# error error -> check fails +# warning error -> check fails +# convention error -> check fails +# refactor warning -> check passes +# info warning -> check passes +# +# also see the RuboCop docs on severity +# https://docs.rubocop.org/rubocop/configuration.html#severity + +# "Rubocop defaults" are by default required/included: +# https://github.com/rubocop/rubocop/blob/master/config/default.yml require: - - rubocop-packaging - rubocop-performance - rubocop-rails AllCops: - TargetRubyVersion: 3.0 - # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop - # to ignore them, so only the ones explicitly set in this file are enabled. - DisabledByDefault: false - Exclude: - - '**/tmp/**/*' - - '**/templates/**/*' - - '**/vendor/**/*' - - 'actionpack/lib/action_dispatch/journey/parser.rb' - - 'actionmailbox/test/dummy/**/*' - - 'actiontext/test/dummy/**/*' - - '**/node_modules/**/*' + # While default cops are automatically included, they are not "configured" + # by default (only on each next major version of RuboCop). Therefore, we + # have to explicitly configure them here. + # see https://docs.rubocop.org/rubocop/configuration.html#defaults + # and https://docs.rubocop.org/rubocop/1.2/versioning.html#pending-cops + NewCops: enable + # Ruby version is determined automatically from the Gemfile.lock + +############################################# +# Layout +############################################# -Performance: - Exclude: - - '**/test/**/*' - -# Prefer assert_not over assert ! -Rails/AssertNot: - Include: - - '**/test/**/*' - -# Prefer assert_not_x over refute_x -Rails/RefuteMethods: - Include: - - '**/test/**/*' - -Rails/IndexBy: - Enabled: true - -Rails/IndexWith: - Enabled: true - -# Prefer &&/|| over and/or. -Style/AndOr: - Enabled: true - -Layout/ArgumentAlignment: - Enabled: true - -Layout/ArrayAlignment: - Enabled: true - -Layout/BlockAlignment: - Enabled: true - -# Align `when` with `case`. -Layout/CaseIndentation: - Enabled: true - -Layout/ClosingHeredocIndentation: - Enabled: true - -# Align comments with method definitions. -Layout/CommentIndentation: - Enabled: true - -Layout/ElseAlignment: - Enabled: true - -# Align `end` with the matching keyword or starting expression except for -# assignments, where it should be aligned with the LHS. Layout/EndAlignment: - Enabled: true EnforcedStyleAlignWith: variable - AutoCorrect: true - -Layout/EmptyLines: - Enabled: true -Layout/EmptyLineAfterMagicComment: - Enabled: true - -Layout/EmptyLinesAroundAccessModifier: - Enabled: true - -Layout/EmptyLinesAroundBlockBody: - Enabled: true - -# In a regular class definition, no empty lines around the body. -Layout/EmptyLinesAroundClassBody: - Enabled: true - -# In a regular method definition, no empty lines around the body. -Layout/EmptyLinesAroundMethodBody: - Enabled: true - -# In a regular module definition, no empty lines around the body. -Layout/EmptyLinesAroundModuleBody: - Enabled: true - -Layout/ExtraSpacing: - Enabled: true - -Layout/LineLength: - Max: 80 - -# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. -Style/HashSyntax: - Enabled: true - -Layout/FirstArgumentIndentation: - Enabled: true - -Layout/HashAlignment: - Enabled: true - -# Method definitions after `private` or `protected` isolated calls need one -# extra level of indentation. Layout/IndentationConsistency: - Enabled: true EnforcedStyle: indented_internal_methods -# Two spaces, no tabs (for indentation). -Layout/IndentationWidth: - Enabled: true - -Layout/LeadingCommentSpace: - Enabled: true - -Layout/MultilineOperationIndentation: - Enabled: true - -Layout/SpaceAfterColon: - Enabled: true +Layout/LineLength: + Max: 100 -Layout/SpaceAfterComma: - Enabled: true +############################################# +# Metrics +############################################# -Layout/SpaceAfterSemicolon: - Enabled: true +Metrics: + Enabled: false -Layout/SpaceAroundEqualsInParameterDefault: +Metrics/ParameterLists: Enabled: true -Layout/SpaceAroundKeyword: - Enabled: true +############################################# +# Performance +############################################# -Layout/SpaceBeforeComma: - Enabled: true +Performance: + Severity: refactor # a warning in CI/CD -Layout/SpaceBeforeComment: - Enabled: true +Performance/FlatMap: + Severity: warning # an error in CI/CD -Layout/SpaceBeforeFirstArg: - Enabled: true -Layout/SpaceInsideArrayLiteralBrackets: - Enabled: true +############################################# +# Rails +############################################# -Style/BlockDelimiters: - Enabled: true +Rails/HasManyOrHasOneDependent: + Enabled: false -Style/BlockComments: - Enabled: true +Rails/SkipsModelValidations: + AllowedMethods: ["touch", "touch_all"] -Style/ConditionalAssignment: - Enabled: true +Rails/UnknownEnv: + Environments: ["development", "test", "production", "docker_development"] -Style/DefWithParentheses: - Enabled: true +############################################# +# Style +############################################# Style/Documentation: - Enabled: true - -# Defining a method with parameters needs parentheses. -Style/MethodDefParentheses: - Enabled: true - -Style/MethodCallWithoutArgsParentheses: - Enabled: true - -Style/FrozenStringLiteralComment: - Enabled: true - EnforcedStyle: always - Exclude: - - 'actionview/test/**/*.builder' - - 'actionview/test/**/*.ruby' - - 'actionpack/test/**/*.builder' - - 'actionpack/test/**/*.ruby' - - 'activestorage/db/migrate/**/*.rb' - - 'activestorage/db/update_migrate/**/*.rb' - - 'actionmailbox/db/migrate/**/*.rb' - - 'actiontext/db/migrate/**/*.rb' - -Style/NumericLiterals: - Enabled: true - -Style/NumericPredicate: - Enabled: true - -Style/RedundantFreeze: - Enabled: true - -Style/SymbolProc: - Enabled: true - -Style/SymbolArray: Enabled: false -# Use `foo {}` not `foo{}`. -Layout/SpaceBeforeBlockBraces: - Enabled: true - -# Use `foo { bar }` not `foo {bar}`. -Layout/SpaceInsideBlockBraces: - Enabled: true - EnforcedStyleForEmptyBraces: space - -# Use `{ a: 1 }` not `{a:1}`. -Layout/SpaceInsideHashLiteralBraces: - Enabled: true - -Layout/SpaceInsideParens: - Enabled: true - -# Check quotes usage according to lint rule below. -Style/StringLiterals: - Enabled: true - EnforcedStyle: single_quotes - -# Detect hard tabs, no hard tabs. -Layout/IndentationStyle: - Enabled: true - -# Empty lines should not have any spaces. -Layout/TrailingEmptyLines: - Enabled: true - -# No trailing whitespace. -Layout/TrailingWhitespace: - Enabled: true - -# Use quotes for string literals when they are enough. -Style/RedundantPercentQ: - Enabled: true +Style/EmptyMethod: + EnforcedStyle: expanded -Lint/AmbiguousOperator: - Enabled: true - -Lint/AmbiguousRegexpLiteral: - Enabled: true - -Lint/ErbNewArguments: - Enabled: true - -# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. -Lint/RequireParentheses: - Enabled: true - -Lint/ShadowingOuterLocalVariable: - Enabled: true - -Lint/RedundantStringCoercion: - Enabled: true - -Lint/UriEscapeUnescape: - Enabled: true - -Lint/UselessAssignment: - Enabled: true - -Lint/DeprecatedClassMethods: - Enabled: true - -Naming/VariableNumber: - Enabled: true - -Style/ParenthesesAroundCondition: - Enabled: true - -Style/HashTransformKeys: - Enabled: true +Style/FrozenStringLiteralComment: + EnforcedStyle: never -Style/HashTransformValues: - Enabled: true +Style/HashSyntax: + EnforcedShorthandSyntax: never -Style/RedundantBegin: +Style/MethodCallWithArgsParentheses: Enabled: true + AllowedMethods: ["authorize!", "authorize", "can", "can?", "head", "import", + "include", "not_to", "puts", "render", "require", "to"] + AllowedPatterns: [^redirect_] + # Don't enforce in migrations, as we have methods like `add_column`, + # `change_column` etc. and parentheses would be very annoying there. + Exclude: ["db/**/*"] Style/RedundantReturn: - Enabled: true AllowMultipleReturnValues: true -Style/Semicolon: - Enabled: true - AllowAsExpressionSeparator: true - -# Prefer Foo.method over Foo::method -Style/ColonMethodCall: - Enabled: true - -Style/TrivialAccessors: - Enabled: true - -Performance/FlatMap: - Enabled: true - -Performance/RedundantMerge: - Enabled: true - -Performance/StartWith: - Enabled: true - -Performance/EndWith: - Enabled: true - -Performance/RegexpMatch: - Enabled: true - -Performance/ReverseEach: - Enabled: true - -Performance/UnfreezeString: - Enabled: true - -Performance/DeletePrefix: - Enabled: true - -Performance/DeleteSuffix: - Enabled: true +Style/StringLiterals: + EnforcedStyle: double_quotes -Metrics/ModuleLength: - Enabled: false +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes -Metrics/ClassLength: - Enabled: false - -Metrics/MethodLength: - Exclude: - - 'app/abilities/**/*' +Style/SymbolArray: + EnforcedStyle: brackets -Metrics/AbcSize: - Exclude: - - 'app/abilities/**/*' \ No newline at end of file +Style/WordArray: + EnforcedStyle: brackets diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..baa13b8bb --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "shopify.ruby-lsp", + "dbaeumer.vscode-eslint", + "streetsidesoftware.code-spell-checker", + "streetsidesoftware.code-spell-checker-german" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..21983ff1e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,87 @@ +{ + "editor.formatOnSave": false, // it still autosaves with the options below + ////////////////////////////////////// + // JS (ESLint) + ////////////////////////////////////// + // https://eslint.style/guide/faq#how-to-auto-format-on-save + // https://github.com/microsoft/vscode-eslint#settings-options + "[javascript]": { + "editor.formatOnSave": false, // to avoid formatting twice (ESLint + VSCode) + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "eslint.format.enable": true, // use ESLint as formatter + "eslint.experimental.useFlatConfig": true, + // this disables VSCode built-in formatter (instead we want to use ESLint) + "javascript.validate.enable": false, + ////////////////////////////////////// + // HTML + ////////////////////////////////////// + "[html]": { + "editor.formatOnSave": false // TODO: activate once HTML formatter installed + }, + ////////////////////////////////////// + // Ruby (Rubocop) + ////////////////////////////////////// + "[ruby]": { + "editor.defaultFormatter": "Shopify.ruby-lsp", + "editor.formatOnSave": true + }, + "rubyLsp.formatter": "rubocop", + "rubyLsp.rubyVersionManager": "rbenv", + "rubyLsp.enabledFeatures": { + "codeActions": true, + "diagnostics": true, + "documentHighlights": true, + "documentLink": true, + "documentSymbols": true, + "foldingRanges": true, + "formatting": true, + "hover": true, + "inlayHint": true, + "onTypeFormatting": true, + "selectionRanges": true, + "semanticHighlighting": true, + "completion": true, + "codeLens": true, + "definition": true + }, + "rubyLsp.enableExperimentalFeatures": true, + ////////////////////////////////////// + // Files + ////////////////////////////////////// + "files.exclude": { + "node_modules/": true, + "pdfcomprezzor/": true, + "coverage/": true, + "solr/": true + }, + "files.associations": { + "*.js.erb": "javascript", + "*.html.erb": "html" + }, + ////////////////////////////////////// + // Editor + ////////////////////////////////////// + "editor.wordWrap": "wordWrapColumn", + "editor.wordWrapColumn": 100, // toggle via Alt + Z shortcut + "editor.mouseWheelZoom": true, + "editor.rulers": [ + { + "column": 80, // soft limit + "color": "#e5e5e5" + }, + { + "column": 100, // hard limit + "color": "#c9c9c9" + } + ], + ////////////////////////////////////// + // Spell Checker + ////////////////////////////////////// + "cSpell.words": [ + "turbolinks" + ] +} \ No newline at end of file diff --git a/Gemfile b/Gemfile index 228b9e901..aa44445ae 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '3.1.4' +ruby "3.1.4" # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem "rails", "~> 7.0.4.3" @@ -32,99 +32,90 @@ gem "jbuilder" # gem 'image_processing', '~> 1.2' # Reduces boot times through caching; required in config/boot.rb +gem "active_model_serializers" gem "bootsnap", ">= 1.4.2", require: false gem "rack" -gem "active_model_serializers" # Use CoffeeScript for .coffee assets and views gem "coffee-rails", "~> 5.0.0" # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 3.0' -gem "shrine" gem "fastimage" -gem "streamio-ffmpeg" -gem "pdf-reader" -gem "mini_magick" gem "image_processing" +gem "mini_magick" +gem "pdf-reader" +gem "shrine" +gem "streamio-ffmpeg" # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' gem "filesize" # Use Capistrano for deployment # gem 'capistrano-rails', group: :development -gem "rgl" -gem "responders" -gem "pg" -gem "devise" -gem "erubis" -gem "cancancan" -gem "jquery-rails" -gem "jquery-ui-rails" -gem "js-routes", '1.4.9' +gem "activerecord-import", + git: "https://github.com/zdennis/activerecord-import.git", + branch: "master" +gem "acts_as_list" +gem "acts_as_tree" +gem "acts_as_votable" +gem "barby" gem "bootstrap", "~>5" gem "bootstrap_form" +gem "cancancan" +gem "clipboard-rails" +gem "commontator" +gem "coveralls", require: false +gem "devise" gem "devise-bootstrap-views" +gem "erubis" +gem "exception_handler", "~> 0.8.0.0" +gem "faraday", "~> 1.8" gem "fuzzy-string-match" -gem "coveralls", require: false +gem "globalize" +gem "globalize-accessors" +gem "jquery-rails" +gem "jquery-ui-rails" +gem "js-routes", "1.4.9" gem "kaminari" -gem "acts_as_list" -gem "acts_as_tree" -gem "activerecord-import", - git: "https://github.com/zdennis/activerecord-import.git", - branch: "master" -gem "thredded" -gem "kramdown-parser-gfm" -gem "thredded-markdown_katex", - git: "https://github.com/thredded/thredded-markdown_katex.git", - branch: "main" -gem "rails-i18n" gem "kaminari-i18n" -gem "trix-rails", require: "trix" -gem "sunspot_rails", - github: 'sunspot/sunspot', - glob: 'sunspot_rails/*.gemspec' -gem "sunspot_solr" +gem "kramdown-parser-gfm" +gem "net-smtp" +gem "pg" +gem "premailer-rails" gem "progress_bar" -gem "barby" +gem "rails-i18n" +gem "responders" +gem "rgl" gem "rqrcode" +gem "rubyzip", "~> 2.3.0" gem "sidekiq" gem "sidekiq-cron", "~> 1.1" -gem "faraday", "~> 1.8" -gem "globalize" -gem "globalize-accessors" -gem "commontator" -gem "acts_as_votable" gem "sprockets-rails", - git: "https://github.com/rails/sprockets-rails", - branch: "master" -gem "premailer-rails" -gem "clipboard-rails" -gem "rubyzip", "~> 2.3.0" -gem "exception_handler", "~> 0.8.0.0" -gem 'webpacker', '~> 5.x' -gem 'net-smtp' - -group :development, :docker_development, :test do - # Call 'byebug' anywhere in the code to stop execution and get a debugger console - gem "byebug", platforms: [:mri, :mingw, :x64_mingw] - gem "rspec-rails" - gem "factory_bot_rails" -end + git: "https://github.com/rails/sprockets-rails", + branch: "master" +gem "sunspot_rails", + github: "sunspot/sunspot", + glob: "sunspot_rails/*.gemspec" +gem "sunspot_solr" +gem "thredded" +gem "thredded-markdown_katex", + git: "https://github.com/thredded/thredded-markdown_katex.git", + branch: "main" +gem "trix-rails", require: "trix" +gem "webpacker", "~> 5.x" group :development, :docker_development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. - gem "web-console", ">= 3.3.0" gem "listen", ">= 3.0.5", "< 3.2" gem "rails-erd" + gem "web-console", ">= 3.3.0" # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + gem "marcel" + gem "pgreset" + gem "rubocop", "~> 1.57", require: false + gem "rubocop-performance", "~> 1.16", require: false + gem "rubocop-rails", "~> 2.22", ">= 2.22.1", require: false gem "spring" gem "spring-watcher-listen", "~> 2.0.0" - gem "rubocop", "~> 1.50", require: false - gem "rubocop-packaging", require: false - gem "rubocop-performance", require: false - gem "rubocop-rails", require: false - gem "erb_lint", require: false - gem "pgreset" - gem "marcel" # gem 'bullet' end @@ -132,15 +123,21 @@ group :test do # Adds support for Capybara system testing and selenium driver gem "selenium-webdriver" # Easy installation and use of web drivers to run system tests with browsers - gem 'webdrivers' - gem 'faker' - gem 'database_cleaner' - gem 'launchy' - gem 'simplecov', require: false + gem "database_cleaner" + gem "faker" + gem "launchy" + gem "simplecov", require: false + gem "webdrivers" end group :test, :development, :docker_development do - gem 'cypress-on-rails', '~> 1.0' - gem 'simplecov-cobertura' + # Call 'byebug' anywhere in the code to stop execution and get a debugger console + gem "byebug", platforms: [:mri, :mingw, :x64_mingw] + gem "factory_bot_rails" + gem "rspec-rails" + + gem "cypress-on-rails", "~> 1.0" + gem "simplecov-cobertura" end -gem 'prometheus_exporter' \ No newline at end of file + +gem "prometheus_exporter" diff --git a/Gemfile.lock b/Gemfile.lock index c1296235d..946c11b31 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -132,13 +132,6 @@ GEM execjs (~> 2.0) barby (0.6.8) bcrypt (3.1.18) - better_html (2.0.1) - actionview (>= 6.0) - activesupport (>= 6.0) - ast (~> 2.0) - erubi (~> 1.4) - parser (>= 2.4) - smart_properties bindex (0.8.1) bootsnap (1.16.0) msgpack (~> 1.2) @@ -205,13 +198,6 @@ GEM unf (>= 0.0.5, < 1.0.0) down (5.4.0) addressable (~> 2.8) - erb_lint (0.4.0) - activesupport - better_html (>= 2.0.1) - parser (>= 2.7.1.4) - rainbow - rubocop - smart_properties erubi (1.12.0) erubis (2.7.0) et-orbi (1.2.7) @@ -324,6 +310,7 @@ GEM kramdown (~> 2.0) kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) + language_server-protocol (3.17.0.3) launchy (2.5.2) addressable (~> 2.8) listen (3.0.8) @@ -373,8 +360,9 @@ GEM orm_adapter (0.5.0) pairing_heap (3.0.0) parallel (1.23.0) - parser (3.2.2.0) + parser (3.2.2.4) ast (~> 2.4.1) + racc pdf-reader (2.11.0) Ascii85 (~> 1.0) afm (~> 0.2.1) @@ -455,7 +443,7 @@ GEM ffi (~> 1.0) redis-client (0.14.1) connection_pool - regexp_parser (2.8.0) + regexp_parser (2.8.2) request_store (1.5.1) rack (>= 1.4) responders (3.1.0) @@ -496,27 +484,26 @@ GEM rspec-mocks (~> 3.11) rspec-support (~> 3.11) rspec-support (3.12.0) - rubocop (1.50.2) + rubocop (1.57.2) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.0, < 2.0) + rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.28.0) + rubocop-ast (1.30.0) parser (>= 3.2.1.0) - rubocop-packaging (0.5.1) - rubocop (>= 0.89, < 2.0) - rubocop-performance (1.10.2) - rubocop (>= 0.90.0, < 2.0) + rubocop-performance (1.19.1) + rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.9.1) + rubocop-rails (2.22.1) activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 0.90.0, < 2.0) + rubocop (>= 1.33.0, < 2.0) ruby-graphviz (1.2.5) rexml ruby-progressbar (1.13.0) @@ -564,7 +551,6 @@ GEM simplecov (~> 0.19) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - smart_properties (1.17.0) spring (2.1.1) spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) @@ -627,7 +613,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.8.2) - unicode-display_width (2.4.2) + unicode-display_width (2.5.0) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.0) @@ -677,7 +663,6 @@ DEPENDENCIES database_cleaner devise devise-bootstrap-views - erb_lint erubis exception_handler (~> 0.8.0.0) factory_bot_rails @@ -716,10 +701,9 @@ DEPENDENCIES rgl rqrcode rspec-rails - rubocop (~> 1.50) - rubocop-packaging - rubocop-performance - rubocop-rails + rubocop (~> 1.57) + rubocop-performance (~> 1.16) + rubocop-rails (~> 2.22, >= 2.22.1) rubyzip (~> 2.3.0) sass-rails (>= 6) selenium-webdriver diff --git a/Rakefile b/Rakefile index e85f91391..9a5ea7383 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,6 @@ # 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_relative 'config/application' +require_relative "config/application" Rails.application.load_tasks diff --git a/app/abilities/clicker_ability.rb b/app/abilities/clicker_ability.rb index 6dfd4d5da..5d048f5c5 100644 --- a/app/abilities/clicker_ability.rb +++ b/app/abilities/clicker_ability.rb @@ -9,7 +9,7 @@ def initialize(user) !user.generic? end - can [:show, :get_votes_count], Clicker + can [:show, :votes_count], Clicker can [:edit, :open, :close, :set_alternatives], Clicker do |clicker, code| (user&.admin? || user == clicker.editor) || code == clicker.code diff --git a/app/abilities/main_ability.rb b/app/abilities/main_ability.rb index 7ced6bc32..157c665d4 100644 --- a/app/abilities/main_ability.rb +++ b/app/abilities/main_ability.rb @@ -1,7 +1,7 @@ class MainAbility include CanCan::Ability - def initialize(user) + def initialize(_user) can :start, :main end end diff --git a/app/abilities/medium_ability.rb b/app/abilities/medium_ability.rb index f173dd07c..01c767b9c 100644 --- a/app/abilities/medium_ability.rb +++ b/app/abilities/medium_ability.rb @@ -9,7 +9,7 @@ def initialize(user) can [:show, :show_comments], Medium do |medium| medium.visible_for_user?(user) && - !(medium.sort.in?(['Question', 'Remark']) && !user.can_edit?(medium)) + !(medium.sort.in?(["Question", "Remark"]) && !user.can_edit?(medium)) end can :inspect, Medium do |medium| @@ -18,7 +18,7 @@ def initialize(user) can [:edit, :update, :enrich, :publish, :destroy, :cancel_publication, :add_item, :add_reference, :add_screenshot, :remove_screenshot, - :import_script_items, :import_manuscript, :get_statistics, + :import_script_items, :import_manuscript, :statistics, :render_medium_tags, :fill_quizzable_area, :fill_reassign_modal], Medium do |medium| user.can_edit?(medium) @@ -46,7 +46,7 @@ def initialize(user) !user.generic? && user.can_edit?(medium) end - can [:register_download], Medium do |medium| + can [:register_download], Medium do |_medium| !user.new_record? end end diff --git a/app/abilities/profile_ability.rb b/app/abilities/profile_ability.rb index 3000ca054..43d263676 100644 --- a/app/abilities/profile_ability.rb +++ b/app/abilities/profile_ability.rb @@ -1,7 +1,7 @@ class ProfileAbility include CanCan::Ability - def initialize(user) + def initialize(_user) clear_aliased_actions can [:edit, :update, :check_for_consent, :add_consent, diff --git a/app/abilities/search_ability.rb b/app/abilities/search_ability.rb index 018392d85..aa4c87890 100644 --- a/app/abilities/search_ability.rb +++ b/app/abilities/search_ability.rb @@ -1,7 +1,7 @@ class SearchAbility include CanCan::Ability - def initialize(user) + def initialize(_user) clear_aliased_actions can :index, :search diff --git a/app/abilities/talk_ability.rb b/app/abilities/talk_ability.rb index fe4e62868..24717b175 100644 --- a/app/abilities/talk_ability.rb +++ b/app/abilities/talk_ability.rb @@ -9,7 +9,7 @@ def initialize(user) end can [:new, :edit, :create, :update, :destroy], Talk do |talk| - (talk.lecture && talk.lecture.edited_by?(user)) || user.admin? + talk.lecture&.edited_by?(user) || user.admin? end can [:assemble, :modify], Talk do |talk| diff --git a/app/abilities/tutorial_ability.rb b/app/abilities/tutorial_ability.rb index f02c683b6..7d7134940 100644 --- a/app/abilities/tutorial_ability.rb +++ b/app/abilities/tutorial_ability.rb @@ -9,11 +9,11 @@ def initialize(user) user.can_update_personell?(tutorial.lecture) end - can :overview, Tutorial do |tutorial, lecture| + can :overview, Tutorial do |_tutorial, lecture| user.editor_or_teacher_in?(lecture) end - can :index, Tutorial do |tutorial, lecture| + can :index, Tutorial do |_tutorial, lecture| user.in?(lecture.tutors) || user.editor_or_teacher_in?(lecture) end diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index 1a4a43a20..ff4d5c580 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -2,4 +2,6 @@ //= link_directory ../javascripts .js //= link_directory ../stylesheets .css //= link commontator/manifest.js -//= link thredded_katex.css \ No newline at end of file +//= link thredded_katex.css +//= link monotile/geometry.js +//= link monotile/hat.js \ No newline at end of file diff --git a/app/assets/images/landing-background.jpg b/app/assets/images/landing-background.jpg deleted file mode 100644 index 8824cdb2d..000000000 Binary files a/app/assets/images/landing-background.jpg and /dev/null differ diff --git a/app/assets/javascripts/_selectize_turbolinks_fix.js b/app/assets/javascripts/_selectize_turbolinks_fix.js index eaef1d817..dfe418cf2 100644 --- a/app/assets/javascripts/_selectize_turbolinks_fix.js +++ b/app/assets/javascripts/_selectize_turbolinks_fix.js @@ -5,130 +5,140 @@ // transfer knowledge about selected items from selectize to html options var resetSelectized; -resetSelectized = function(index, select) { +resetSelectized = function (index, select) { var i, len, selectedValue, val; selectedValue = select.tomselect.getValue(); select.tomselect.destroy(); - $(select).find('option').attr('selected', null); - if ($(select).prop('multiple')) { + $(select).find("option").attr("selected", null); + if ($(select).prop("multiple")) { for (i = 0, len = selectedValue.length; i < len; i++) { val = selectedValue[i]; - if (val !== '') { - $(select).find("option[value='" + val + "']").attr('selected', true); + if (val !== "") { + $(select).find("option[value='" + val + "']").attr("selected", true); } } - } else { - if (selectedValue !== '') { - $(select).find("option[value='" + selectedValue + "']").attr('selected', true); + } + else { + if (selectedValue !== "") { + $(select).find("option[value='" + selectedValue + "']").attr("selected", true); } } }; -this.fillOptionsByAjax = function($selectizedSelection) { - $selectizedSelection.each(function() { - var courseId, existing_values, fill_path, loaded, locale, model_select, plugins, send_data, parent; - if (this.dataset.drag === 'true') { - plugins = ['remove_button', 'drag_drop']; - } else { - plugins = ['remove_button']; +function fillOptionsByAjax($selectizedSelection) { + // TODO: this function definitely needs some refactoring + $selectizedSelection.each(function () { + let plugins = []; + let send_data = false; + let fill_path = ""; + let courseId = 0; + let loaded = false; + let locale = null; + + if (this.dataset.drag === "true") { + plugins = ["remove_button", "drag_drop"]; + } + else { + plugins = ["remove_button"]; } - if (this.dataset.ajax === 'true' && this.dataset.filled === 'false') { - model_select = this; + if (this.dataset.ajax === "true" && this.dataset.filled === "false") { + const model_select = this; courseId = 0; - placeholder = this.dataset.placeholder; - no_result_msg = this.dataset.noResults; - existing_values = Array.apply(null, model_select.options).map(function(o) { - return o.value; - }); + const placeholder = this.dataset.placeholder; + const no_result_msg = this.dataset.noResults; send_data = false; loaded = false; - parent = this.dataset.modal === undefined ? document.body : null; - if (this.dataset.model === 'tag') { + if (this.dataset.model === "tag") { locale = this.dataset.locale; fill_path = Routes.fill_tag_select_path({ - locale: locale + locale: locale, }); send_data = true; - } else if (this.dataset.model === 'user') { + } + else if (this.dataset.model === "user") { fill_path = Routes.fill_user_select_path(); send_data = true; - } else if (this.dataset.model === 'user_generic') { + } + else if (this.dataset.model === "user_generic") { fill_path = Routes.list_generic_users_path(); - } else if (this.dataset.model === 'teachable') { + } + else if (this.dataset.model === "teachable") { fill_path = Routes.fill_teachable_select_path(); - } else if (this.dataset.model === 'medium') { + } + else if (this.dataset.model === "medium") { fill_path = Routes.fill_media_select_path(); - } else if (this.dataset.model === 'course_tag') { + } + else if (this.dataset.model === "course_tag") { courseId = this.dataset.course; fill_path = Routes.fill_course_tags_path(); } - (function() { - class MinimumLengthSelect extends TomSelect{ - - refreshOptions(triggerDropdown=true){ + (function () { + class MinimumLengthSelect extends TomSelect { + refreshOptions(triggerDropdown = true) { var query = this.inputValue(); - if( query.length < 2){ + if (query.length < 2) { this.close(false); return; } super.refreshOptions(triggerDropdown); } - } new MinimumLengthSelect("#" + model_select.id, { - plugins: plugins, - valueField: 'value', - labelField: 'name', - searchField: 'name', - maxOptions: null, - placeholder: placeholder, - closeAfterSelect: true, - load: function(query, callback) { - var url; - if (send_data || !loaded) { - url = fill_path + "?course_id=" + courseId + "&q=" + encodeURIComponent(query); - fetch(url).then(function(response) { - return response.json(); - }).then(function(json) { - loaded = true; - return callback(json.map(function(item) { - return { - name: item.text, - value: item.value - }; - })); - })["catch"](function() { - callback(); - }); - } - callback(); - }, - render: { - option: function(data, escape) { - return '
' + '' + escape(data.name) + '' + '
'; + plugins: plugins, + valueField: "value", + labelField: "name", + searchField: "name", + maxOptions: null, + placeholder: placeholder, + closeAfterSelect: true, + load: function (query, callback) { + var url; + if (send_data || !loaded) { + url = fill_path + "?course_id=" + courseId + "&q=" + encodeURIComponent(query); + fetch(url).then(function (response) { + return response.json(); + }).then(function (json) { + loaded = true; + return callback(json.map(function (item) { + return { + name: item.text, + value: item.value, + }; + })); + })["catch"](function () { + callback(); + }); + } + callback(); }, - item: function(item, escape) { - return '
' + escape(item.name) + '
'; + render: { + option: function (data, escape) { + return "
" + '' + escape(data.name) + "" + "
"; + }, + item: function (item, escape) { + return '
' + escape(item.name) + "
"; + }, + no_results: function (data, escape) { + return '
' + escape(no_result_msg) + "
"; + }, }, - no_results: function(data, escape) { - return '
'+ escape(no_result_msg) + '
'; - } - } - }); - })();} else { + }); + })(); + } + else { return new TomSelect("#" + this.id, { plugins: plugins, - maxOptions: null + maxOptions: null, }); } }); -}; +} -$(document).on('turbolinks:before-cache', function() { - $('.tomselected').each(resetSelectized); +$(document).on("turbolinks:before-cache", function () { + $(".tomselected").each(resetSelectized); }); -$(document).on('turbolinks:load', function() { - fillOptionsByAjax($('.selectize')); +$(document).on("turbolinks:load", function () { + fillOptionsByAjax($(".selectize")); }); diff --git a/app/assets/javascripts/bootstrap_modal_turbolinks_fix.js b/app/assets/javascripts/bootstrap_modal_turbolinks_fix.js index d050707d7..15f5d432d 100644 --- a/app/assets/javascripts/bootstrap_modal_turbolinks_fix.js +++ b/app/assets/javascripts/bootstrap_modal_turbolinks_fix.js @@ -1,16 +1,16 @@ -$(document).on('turbolinks:load', function () { - // show all active modals - $('.activeModal').modal('show'); - // remove active status (this needs to be reestablished before caching) - $('.activeModal').removeClass('activeModal'); +$(document).on("turbolinks:load", function () { + // show all active modals + $(".activeModal").modal("show"); + // remove active status (this needs to be reestablished before caching) + $(".activeModal").removeClass("activeModal"); }); -$(document).on('turbolinks:before-cache', function () { - // if some modal is open - if ($('body').hasClass('modal-open')) { - $('.modal.show').addClass('activeModal'); - $('.modal.show').modal('hide'); - // remove the greyed out background - $('.modal-backdrop').remove(); - } +$(document).on("turbolinks:before-cache", function () { + // if some modal is open + if ($("body").hasClass("modal-open")) { + $(".modal.show").addClass("activeModal"); + $(".modal.show").modal("hide"); + // remove the greyed out background + $(".modal-backdrop").remove(); + } }); diff --git a/app/assets/javascripts/bootstrap_popovers.js b/app/assets/javascripts/bootstrap_popovers.js index 8bc3d7cd9..16cfe7d5c 100644 --- a/app/assets/javascripts/bootstrap_popovers.js +++ b/app/assets/javascripts/bootstrap_popovers.js @@ -1,18 +1,18 @@ -$(document).on('turbolinks:load', function () { - initBootstrapPopovers(); +$(document).on("turbolinks:load", function () { + initBootstrapPopovers(); }); /** * Initializes all Bootstrap popovers on the page. - * + * * This function might be used for the first initialization of popovers as well * as for reinitialization on page changes. * * See: https://getbootstrap.com/docs/5.3/components/popovers/#enable-popovers */ function initBootstrapPopovers() { - const popoverHtmlElements = document.querySelectorAll('[data-bs-toggle="popover"]'); - for (const element of popoverHtmlElements) { - new bootstrap.Popover(element); - } -} \ No newline at end of file + const popoverHtmlElements = document.querySelectorAll('[data-bs-toggle="popover"]'); + for (const element of popoverHtmlElements) { + new bootstrap.Popover(element); + } +} diff --git a/app/assets/javascripts/cable.js b/app/assets/javascripts/cable.js index 739aa5f02..170e304a6 100644 --- a/app/assets/javascripts/cable.js +++ b/app/assets/javascripts/cable.js @@ -1,13 +1,15 @@ // Action Cable provides the framework to deal with WebSockets in Rails. // You can generate new channels where WebSocket features live using the `rails generate channel` command. // +// disable eslint +/* eslint-disable */ //= require action_cable //= require_self //= require_tree ./channels +/* eslint-enable */ -(function() { +(function () { this.App || (this.App = {}); App.cable = ActionCable.createConsumer(); - }).call(this); diff --git a/app/assets/javascripts/datetimepicker.js b/app/assets/javascripts/datetimepicker.js index 7730f99a1..9d6376d70 100644 --- a/app/assets/javascripts/datetimepicker.js +++ b/app/assets/javascripts/datetimepicker.js @@ -1,126 +1,129 @@ +/* global tempusDominus */ + // Initialize on page load (when js file is dynamically loaded) $(document).ready(startInitialization); // On page change (e.g. go back and forth in browser) -$(document).on('turbolinks:before-cache', () => { - // Remove stale datetimepickers - $('.tempus-dominus-widget').remove(); +$(document).on("turbolinks:before-cache", () => { + // Remove stale datetimepickers + $(".tempus-dominus-widget").remove(); }); function startInitialization() { - const pickerElements = $('.td-picker'); - if (pickerElements.length == 0) { - console.error('No datetimepicker element found on page, although requested.'); - return; - } + const pickerElements = $(".td-picker"); + if (pickerElements.length == 0) { + console.error("No datetimepicker element found on page, although requested."); + return; + } - pickerElements.each((i, element) => { - element = $(element); - const datetimePicker = initDatetimePicker(element); - registerErrorHandlers(datetimePicker, element); - registerFocusHandlers(datetimePicker, element); - }); + pickerElements.each((i, element) => { + element = $(element); + const datetimePicker = initDatetimePicker(element); + registerErrorHandlers(datetimePicker, element); + registerFocusHandlers(datetimePicker, element); + }); } function getDateTimePickerIcons() { - // At the moment: continue to use FontAwesome 5 icons - // see https://getdatepicker.com/6/plugins/fa5.html - // see https://github.com/Eonasdan/tempus-dominus/blob/master/dist/plugins/fa-five.js - return { - type: 'icons', - time: 'fas fa-clock', - date: 'fas fa-calendar', - up: 'fas fa-arrow-up', - down: 'fas fa-arrow-down', - previous: 'fas fa-chevron-left', - next: 'fas fa-chevron-right', - today: 'fas fa-calendar-check', - clear: 'fas fa-trash', - close: 'fas fa-times', - } + // At the moment: continue to use FontAwesome 5 icons + // see https://getdatepicker.com/6/plugins/fa5.html + // see https://github.com/Eonasdan/tempus-dominus/blob/master/dist/plugins/fa-five.js + return { + type: "icons", + time: "fas fa-clock", + date: "fas fa-calendar", + up: "fas fa-arrow-up", + down: "fas fa-arrow-down", + previous: "fas fa-chevron-left", + next: "fas fa-chevron-right", + today: "fas fa-calendar-check", + clear: "fas fa-trash", + close: "fas fa-times", + }; } function initDatetimePicker(element) { - // see https://getdatepicker.com - return new tempusDominus.TempusDominus( - element.get(0), - { - display: { - sideBySide: true, // clock to the right of the calendar - icons: getDateTimePickerIcons(), - }, - localization: { - startOfTheWeek: 1, - // choose format to be compliant with backend time format - format: 'yyyy-MM-dd HH:mm', - hourCycle: 'h23', - } - } - ); + // see https://getdatepicker.com + return new tempusDominus.TempusDominus( + element.get(0), + { + display: { + sideBySide: true, // clock to the right of the calendar + icons: getDateTimePickerIcons(), + }, + localization: { + startOfTheWeek: 1, + // choose format to be compliant with backend time format + format: "yyyy-MM-dd HH:mm", + hourCycle: "h23", + }, + }, + ); } function registerErrorHandlers(datetimePicker, element) { - // Catch Tempus Dominus error when user types in invalid date - // this is rather hacky at the moment, see this discussion: - // https://github.com/Eonasdan/tempus-dominus/discussions/2656 - datetimePicker.dates.oldParseInput = datetimePicker.dates.parseInput; - datetimePicker.dates.parseInput = (input) => { - try { - return datetimePicker.dates.oldParseInput(input); - } catch (err) { - const errorMsg = element.find('.td-error').data('td-invalid-date'); - element.find('.td-error').text(errorMsg).show(); - datetimePicker.dates.clear(); - } - }; + // Catch Tempus Dominus error when user types in invalid date + // this is rather hacky at the moment, see this discussion: + // https://github.com/Eonasdan/tempus-dominus/discussions/2656 + datetimePicker.dates.oldParseInput = datetimePicker.dates.parseInput; + datetimePicker.dates.parseInput = (input) => { + try { + return datetimePicker.dates.oldParseInput(input); + } + catch (err) { + const errorMsg = element.find(".td-error").data("td-invalid-date"); + element.find(".td-error").text(errorMsg).show(); + datetimePicker.dates.clear(); + } + }; - datetimePicker.subscribe(tempusDominus.Namespace.events.change, (e) => { - // see https://getdatepicker.com/6/namespace/events.html#change + datetimePicker.subscribe(tempusDominus.Namespace.events.change, (e) => { + // see https://getdatepicker.com/6/namespace/events.html#change - // Clear error message - if (e.isValid && !e.isClear) { - element.find('.td-error').empty(); - } + // Clear error message + if (e.isValid && !e.isClear) { + element.find(".td-error").empty(); + } - // If date was selected, close datetimepicker. - // However: leave the datetimepicker open if user only changed time - if (e.oldDate && e.date && !hasUserChangedDate(e.oldDate, e.date)) { - datetimePicker.hide(); - } - }); + // If date was selected, close datetimepicker. + // However: leave the datetimepicker open if user only changed time + if (e.oldDate && e.date && !hasUserChangedDate(e.oldDate, e.date)) { + datetimePicker.hide(); + } + }); } function hasUserChangedDate(oldDate, newDate) { - return oldDate.getHours() != newDate.getHours() - || oldDate.getMinutes() != newDate.getMinutes(); + return oldDate.getHours() != newDate.getHours() + || oldDate.getMinutes() != newDate.getMinutes(); } function registerFocusHandlers(datetimePicker, element) { - // Show datetimepicker when user clicks in text field next to button - // or when input field receives focus - var isButtonInvokingFocus = false; + // Show datetimepicker when user clicks in text field next to button + // or when input field receives focus + var isButtonInvokingFocus = false; - element.find('.td-input').on('click focusin', (e) => { - try { - if (!isButtonInvokingFocus) { - datetimePicker.show(); - } - } - finally { - isButtonInvokingFocus = false; - } - }); + element.find(".td-input").on("click focusin", (_e) => { + try { + if (!isButtonInvokingFocus) { + datetimePicker.show(); + } + } + finally { + isButtonInvokingFocus = false; + } + }); - element.find('.td-picker-button').on('click', () => { - isButtonInvokingFocus = true; - element.find('.td-input').focus(); - }); + element.find(".td-picker-button").on("click", () => { + isButtonInvokingFocus = true; + element.find(".td-input").focus(); + }); - // Hide datetimepicker when input field loses focus - element.find('.td-input').blur((e) => { - if (!e.relatedTarget) { - return; - } - datetimePicker.hide(); - }); + // Hide datetimepicker when input field loses focus + element.find(".td-input").blur((e) => { + if (!e.relatedTarget) { + return; + } + datetimePicker.hide(); + }); } diff --git a/app/assets/javascripts/footer_modal.js b/app/assets/javascripts/footer_modal.js index 28d02804f..523aa3785 100644 --- a/app/assets/javascripts/footer_modal.js +++ b/app/assets/javascripts/footer_modal.js @@ -1,5 +1,5 @@ document.addEventListener("turbolinks:load", () => { - if (window.location.hash == "#sponsors") { - $('#sponsors').modal('show'); - } + if (window.location.hash == "#sponsors") { + $("#sponsors").modal("show"); + } }); diff --git a/app/assets/javascripts/media.coffee b/app/assets/javascripts/media.coffee index 8869ee596..3acd8d046 100644 --- a/app/assets/javascripts/media.coffee +++ b/app/assets/javascripts/media.coffee @@ -378,7 +378,7 @@ $(document).on 'turbolinks:load', -> $(document).on 'click', '#showMediaStatistics', -> mediumId = $(this).data('medium') - $.ajax Routes.get_statistics_path(mediumId), + $.ajax Routes.statistics_path(mediumId), type: 'GET' dataType: 'script' return diff --git a/app/assets/javascripts/monotile/geometry.js b/app/assets/javascripts/monotile/geometry.js new file mode 100644 index 000000000..aee489858 --- /dev/null +++ b/app/assets/javascripts/monotile/geometry.js @@ -0,0 +1,94 @@ +// BSD-3-Clause licensed by Craig S. Kaplan +// adapted from: https://github.com/isohedral/hatviz + +// This file is mostly code that will get replaced anyways since +// the monotiles will probably not stay on the front screen forever. +// Having to properly use modules here and import/export the respective variables +// would be overkill. This is mostly external code from the monotile project. +// It works as intended and is pretty much unrelated to the rest of our code base. +// For these reasons, we disable some ESLint rules for this file. +/* eslint-disable no-undef, no-unused-vars */ + +const r3 = 1.7320508075688772; +const hr3 = 0.8660254037844386; +const ident = [1, 0, 0, 0, 1, 0]; + +function pt(x, y) { + return { x: x, y: y }; +} + +function hexPt(x, y) { + return pt(x + 0.5 * y, hr3 * y); +} + +// Affine matrix inverse +function inv(T) { + const det = T[0] * T[4] - T[1] * T[3]; + return [T[4] / det, -T[1] / det, (T[1] * T[5] - T[2] * T[4]) / det, + -T[3] / det, T[0] / det, (T[2] * T[3] - T[0] * T[5]) / det]; +} + +// Affine matrix multiply +function mul(A, B) { + return [A[0] * B[0] + A[1] * B[3], + A[0] * B[1] + A[1] * B[4], + A[0] * B[2] + A[1] * B[5] + A[2], + + A[3] * B[0] + A[4] * B[3], + A[3] * B[1] + A[4] * B[4], + A[3] * B[2] + A[4] * B[5] + A[5]]; +} + +function padd(p, q) { + return { x: p.x + q.x, y: p.y + q.y }; +} + +function psub(p, q) { + return { x: p.x - q.x, y: p.y - q.y }; +} + +// Rotation matrix +function trot(ang) { + const c = cos(ang); + const s = sin(ang); + return [c, -s, 0, s, c, 0]; +} + +// Translation matrix +function ttrans(tx, ty) { + return [1, 0, tx, 0, 1, ty]; +} + +function rotAbout(p, ang) { + return mul(ttrans(p.x, p.y), + mul(trot(ang), ttrans(-p.x, -p.y))); +} + +// Matrix * point +function transPt(M, P) { + return pt(M[0] * P.x + M[1] * P.y + M[2], M[3] * P.x + M[4] * P.y + M[5]); +} + +// Match unit interval to line segment p->q +function matchSeg(p, q) { + return [q.x - p.x, p.y - q.y, p.x, q.y - p.y, q.x - p.x, p.y]; +} + +// Match line segment p1->q1 to line segment p2->q2 +function matchTwo(p1, q1, p2, q2) { + return mul(matchSeg(p2, q2), inv(matchSeg(p1, q1))); +} + +// Intersect two lines defined by segments p1->q1 and p2->q2 +function intersect(p1, q1, p2, q2) { + const d = (q2.y - p2.y) * (q1.x - p1.x) - (q2.x - p2.x) * (q1.y - p1.y); + const uA = ((q2.x - p2.x) * (p1.y - p2.y) - (q2.y - p2.y) * (p1.x - p2.x)) / d; + return pt(p1.x + uA * (q1.x - p1.x), p1.y + uA * (q1.y - p1.y)); +} + +const hat_outline = [ + hexPt(0, 0), hexPt(-1, -1), hexPt(0, -2), hexPt(2, -2), + hexPt(2, -1), hexPt(4, -2), hexPt(5, -1), hexPt(4, 0), + hexPt(3, 0), hexPt(2, 2), hexPt(0, 3), hexPt(0, 2), + hexPt(-1, 2), +]; diff --git a/app/assets/javascripts/monotile/hat.js b/app/assets/javascripts/monotile/hat.js new file mode 100644 index 000000000..820116752 --- /dev/null +++ b/app/assets/javascripts/monotile/hat.js @@ -0,0 +1,479 @@ +// BSD-3-Clause licensed by Craig S. Kaplan +// adapted from: https://github.com/isohedral/hatviz + +// This file is mostly code that will get replaced anyways since +// the monotiles will probably not stay on the front screen forever. +// Having to properly use modules here and import/export the respective variables +// would be overkill. This is mostly external code from the monotile project. +// It works as intended and is pretty much unrelated to the rest of our code base. +// For these reasons, we disable some ESLint rules for this file. +/* eslint-disable no-undef, no-unused-vars */ + +// A bit of a "hacky" fix. This is the only way I found that works +// so that the canvas is loaded even if the site is left +// e.g. user clicks on "Register" or changes language from "De" to "En" +// with the button on the landing page. +// Also see the following link for a possible workaround, which does not work +// for us in conjunction with turbolink: +// https://github.com/processing/p5.js/wiki/p5.js-overview#why-cant-i-assign-variables-using-p5-functions-and-variables-before-setup +$(document).on("turbolinks:load", () => { + try { + setup(); + } + catch (err) { + if (err instanceof ReferenceError) { + return; + } + console.error(err); + } +}); + +const INITIAL_TO_SCREEN = [45, 0, 0, 0, -45, 0]; +let to_screen = INITIAL_TO_SCREEN; +let tiles; +let level; +let box_height; + +let monotile_btn; +let reset_btn; + +const colors = { + H1: [0, 137, 212], + H: [148, 205, 235], + T: [251, 251, 251], + P: [250, 250, 250], + F: [220, 220, 220], +}; +let black = [0, 0, 0]; + +function drawPolygon(shape, T, f, s, w) { + if (f != null) { + fill(f); + } + else { + noFill(); + } + if (s != null) { + stroke(s); + strokeWeight(w); + } + else { + noStroke(); + } + beginShape(); + for (let p of shape) { + const tp = transPt(T, p); + vertex(tp.x, tp.y); + } + endShape(CLOSE); +} + +// The base level of the scene, a single hat tile, including a label +// for colouring +class HatTile { + constructor(label) { + this.label = label; + } + + draw(S, level) { + drawPolygon( + hat_outline, S, colors[this.label], black, 1); + } +} + +// A group that collects a list of transformed children and an outline +class MetaTile { + constructor(shape, width) { + this.shape = shape; + this.width = width; + this.children = []; + } + + addChild(T, geom) { + this.children.push({ T: T, geom: geom }); + } + + evalChild(n, i) { + return transPt(this.children[n].T, this.children[n].geom.shape[i]); + } + + draw(S, level) { + if (level > 0) { + for (let g of this.children) { + g.geom.draw(mul(S, g.T), level - 1); + } + } + else { + drawPolygon(this.shape, S, null, black, this.width); + } + } + + recentre() { + let cx = 0; + let cy = 0; + for (let p of this.shape) { + cx += p.x; + cy += p.y; + } + cx /= this.shape.length; + cy /= this.shape.length; + const tr = pt(-cx, -cy); + + for (let idx = 0; idx < this.shape.length; ++idx) { + this.shape[idx] = padd(this.shape[idx], tr); + } + + const M = ttrans(-cx, -cy); + for (let ch of this.children) { + ch.T = mul(M, ch.T); + } + } +} + +const H1_hat = new HatTile("H1"); +const H_hat = new HatTile("H"); +const T_hat = new HatTile("T"); +const P_hat = new HatTile("P"); +const F_hat = new HatTile("F"); + +function initH() { + const H_outline = [ + pt(0, 0), pt(4, 0), pt(4.5, hr3), + pt(2.5, 5 * hr3), pt(1.5, 5 * hr3), pt(-0.5, hr3)]; + const meta = new MetaTile(H_outline, 2); + + meta.addChild( + matchTwo( + hat_outline[5], hat_outline[7], H_outline[5], H_outline[0]), + H_hat); + meta.addChild( + matchTwo( + hat_outline[9], hat_outline[11], H_outline[1], H_outline[2]), + H_hat); + meta.addChild( + matchTwo( + hat_outline[5], hat_outline[7], H_outline[3], H_outline[4]), + H_hat); + meta.addChild( + mul(ttrans(2.5, hr3), + mul( + [-0.5, -hr3, 0, hr3, -0.5, 0], + [0.5, 0, 0, 0, -0.5, 0])), + H1_hat); + + return meta; +} + +function initT() { + const T_outline = [ + pt(0, 0), pt(3, 0), pt(1.5, 3 * hr3)]; + const meta = new MetaTile(T_outline, 2); + + meta.addChild( + [0.5, 0, 0.5, 0, 0.5, hr3], + T_hat); + + return meta; +} + +function initP() { + const P_outline = [ + pt(0, 0), pt(4, 0), + pt(3, 2 * hr3), + pt(-1, 2 * hr3), + ]; + const meta = new MetaTile(P_outline, 2); + + meta.addChild([0.5, 0, 1.5, 0, 0.5, hr3], P_hat); + meta.addChild( + mul(ttrans(0, 2 * hr3), + mul([0.5, hr3, 0, -hr3, 0.5, 0], + [0.5, 0.0, 0.0, 0.0, 0.5, 0.0])), + P_hat); + + return meta; +} + +function initF() { + const F_outline = [ + pt(0, 0), pt(3, 0), + pt(3.5, hr3), pt(3, 2 * hr3), pt(-1, 2 * hr3), + ]; + const meta = new MetaTile(F_outline, 2); + + meta.addChild( + [0.5, 0, 1.5, 0, 0.5, hr3], + F_hat); + meta.addChild( + mul(ttrans(0, 2 * hr3), + mul([0.5, hr3, 0, -hr3, 0.5, 0], + [0.5, 0.0, 0.0, 0.0, 0.5, 0.0])), + F_hat); + + return meta; +} + +function constructPatch(H, T, P, F) { + const rules = [ + ["H"], + [0, 0, "P", 2], + [1, 0, "H", 2], + [2, 0, "P", 2], + [3, 0, "H", 2], + [4, 4, "P", 2], + [0, 4, "F", 3], + [2, 4, "F", 3], + [4, 1, 3, 2, "F", 0], + [8, 3, "H", 0], + [9, 2, "P", 0], + [10, 2, "H", 0], + [11, 4, "P", 2], + [12, 0, "H", 2], + [13, 0, "F", 3], + [14, 2, "F", 1], + [15, 3, "H", 4], + [8, 2, "F", 1], + [17, 3, "H", 0], + [18, 2, "P", 0], + [19, 2, "H", 2], + [20, 4, "F", 3], + [20, 0, "P", 2], + [22, 0, "H", 2], + [23, 4, "F", 3], + [23, 0, "F", 3], + [16, 0, "P", 2], + [9, 4, 0, 2, "T", 2], + [4, 0, "F", 3], + ]; + + ret = new MetaTile([], H.width); + shapes = { H: H, T: T, P: P, F: F }; + + for (let r of rules) { + if (r.length == 1) { + ret.addChild(ident, shapes[r[0]]); + } + else if (r.length == 4) { + const poly = ret.children[r[0]].geom.shape; + const T = ret.children[r[0]].T; + const P = transPt(T, poly[(r[1] + 1) % poly.length]); + const Q = transPt(T, poly[r[1]]); + const nshp = shapes[r[2]]; + const npoly = nshp.shape; + + ret.addChild( + matchTwo(npoly[r[3]], npoly[(r[3] + 1) % npoly.length], P, Q), + nshp); + } + else { + const chP = ret.children[r[0]]; + const chQ = ret.children[r[2]]; + + const P = transPt(chQ.T, chQ.geom.shape[r[3]]); + const Q = transPt(chP.T, chP.geom.shape[r[1]]); + const nshp = shapes[r[4]]; + const npoly = nshp.shape; + + ret.addChild( + matchTwo(npoly[r[5]], npoly[(r[5] + 1) % npoly.length], P, Q), + nshp); + } + } + + return ret; +} + +// You can read the paper preprint if you'd like to know how this works: +// https://arxiv.org/abs/2303.10798 +function constructMetatiles(patch) { + const bps1 = patch.evalChild(8, 2); + const bps2 = patch.evalChild(21, 2); + const rbps = transPt(rotAbout(bps1, -2.0 * PI / 3.0), bps2); + + const p72 = patch.evalChild(7, 2); + const p252 = patch.evalChild(25, 2); + + const llc = intersect(bps1, rbps, + patch.evalChild(6, 2), p72); + let w = psub(patch.evalChild(6, 2), llc); + + const new_H_outline = [llc, bps1]; + w = transPt(trot(-PI / 3), w); + new_H_outline.push(padd(new_H_outline[1], w)); + new_H_outline.push(patch.evalChild(14, 2)); + w = transPt(trot(-PI / 3), w); + new_H_outline.push(psub(new_H_outline[3], w)); + new_H_outline.push(patch.evalChild(6, 2)); + + const new_H = new MetaTile(new_H_outline, patch.width * 2); + for (let ch of [0, 9, 16, 27, 26, 6, 1, 8, 10, 15]) { + new_H.addChild(patch.children[ch].T, patch.children[ch].geom); + } + + const new_P_outline = [p72, padd(p72, psub(bps1, llc)), bps1, llc]; + const new_P = new MetaTile(new_P_outline, patch.width * 2); + for (let ch of [7, 2, 3, 4, 28]) { + new_P.addChild(patch.children[ch].T, patch.children[ch].geom); + } + + const new_F_outline = [ + bps2, patch.evalChild(24, 2), patch.evalChild(25, 0), + p252, padd(p252, psub(llc, bps1))]; + const new_F = new MetaTile(new_F_outline, patch.width * 2); + for (let ch of [21, 20, 22, 23, 24, 25]) { + new_F.addChild(patch.children[ch].T, patch.children[ch].geom); + } + + const AAA = new_H_outline[2]; + const BBB = padd(new_H_outline[1], + psub(new_H_outline[4], new_H_outline[5])); + const CCC = transPt(rotAbout(BBB, -PI / 3), AAA); + const new_T_outline = [BBB, CCC, AAA]; + const new_T = new MetaTile(new_T_outline, patch.width * 2); + new_T.addChild(patch.children[11].T, patch.children[11].geom); + + new_H.recentre(); + new_P.recentre(); + new_F.recentre(); + new_T.recentre(); + + return [new_H, new_T, new_P, new_F]; +} + +function isButtonActive(but) { + return but.elt.style.border.length > 0; +} + +function setButtonActive(but, b) { + but.elt.style.border = (b ? "3px solid black" : ""); +} + +function addButton(name, f) { + const ret = createButton(name); + ret.position(10, box_height); + ret.size(125, 25); + ret.mousePressed(f); + box_height += 40; + + return ret; +} + +function setup() { + box_height = 10; + + let canvas = createCanvas(windowWidth, windowHeight); + canvas.id("einstein-monotile-canvas"); + canvas.parent("einstein-monotile"); + + tiles = [initH(), initT(), initP(), initF()]; + level = 1; + + // Reset button + reset_btn = addButton("Reset", () => reset()); + reset_btn.class("btn btn-light monotile-btn"); + + // Monotile button + monotile_btn = addButton("Monotile", () => monotile()); + monotile_btn.class("btn btn-light monotile-btn disabled"); + + // Find out more button + info_btn = addButton("Info", () => { + window.open("https://cs.uwaterloo.ca/~csk/hat/", "_blank"); + }); + info_btn.class("btn btn-light monotile-btn"); + + // Little animation at the beginning + monotile(); + monotile(); + monotile(); +} + +function reset() { + tiles = [initH(), initT(), initP(), initF()]; + level = 1; + to_screen = INITIAL_TO_SCREEN; + monotile(); + monotile_btn.removeClass("disabled"); + loop(); +} + +function monotile() { + // Don't go past a certain level to avoid using too much memory + // and therefore crashing the browser. + if (level == 5) { + monotile_btn.addClass("disabled"); + return; + } + + const patch = constructPatch(...tiles); + tiles = constructMetatiles(patch); + level++; + loop(); +} + +function draw() { + background(255); // white background + push(); + translate(width / 2, height / 2); + tiles[0].draw(to_screen, level); + pop(); + + // Draw black overlay + fill(0, 0, 0, 70); + noStroke(); + rect(0, 0, windowWidth, windowHeight); + + noLoop(); +} + +function windowResized() { + resizeCanvas(windowWidth, windowHeight); +} + +/* Mouse movement */ +let dragging = false; +DELTA_THRESHOLD = 5; + +function mousePressed() { + dragging = true; + loop(); +} + +function touchMoved() { + // Do nothing. + // This prevents the canvas from being draggable on mobile devices + // where this would lead to the page not being able to scroll at all. + // see more: https://p5js.org/reference/#/p5/touchMoved +} + +function mouseDragged(event) { + if (!dragging) { + return true; + } + + // Only move if the mouse has moved a certain amount + const diffX = mouseX - pmouseX; + const diffY = mouseY - pmouseY; + if (diffX <= DELTA_THRESHOLD && diffY == DELTA_THRESHOLD) { + return true; + } + + // Recalculate the transformation matrix + to_screen = mul(ttrans(diffX, diffY), to_screen); + + loop(); + return false; +} + +$(document).ready(function () { + // Prevent mousemove on divs to propagate to canvas + $("#signin-box").on("mousemove", (event) => { + event.stopPropagation(); + }); + $("#footer-bar").on("mousemove", (event) => { + event.stopPropagation(); + }); + $("#announcement-box").on("mousemove", (event) => { + event.stopPropagation(); + }); +}); diff --git a/app/assets/stylesheets/landing.scss b/app/assets/stylesheets/landing.scss index ea65b186a..01bd03317 100644 --- a/app/assets/stylesheets/landing.scss +++ b/app/assets/stylesheets/landing.scss @@ -15,10 +15,14 @@ } #signin-box { + z-index: 2; padding: 70px 40px; + background: linear-gradient(7deg, + rgba(255, 255, 255, 0.85) 0%, white 70%); } #announcement-box { + z-index: 1; display: flex; align-items: baseline; flex-wrap: wrap; @@ -49,9 +53,12 @@ } } -#landing-body { - background-image: image-url('landing-background.jpg'); - background-size: cover; +footer { + z-index: 1; +} + +#footer-bar { + background-color: rgba(255, 255, 255, 0.95); } .ul-no-indentation { @@ -92,4 +99,34 @@ #remember-me-text { white-space: normal; display: contents; +} + +#einstein-monotile-canvas { + position: fixed; + top: 0; + left: 0; + z-index: 0; + cursor: grab; +} + +.monotile-btn { + background-color: rgba(255, 255, 255, 0.4); + height: 35px !important; + font-weight: 400; + + &:hover { + background-color: white; + } + + &.disabled { + background-color: rgba(255, 255, 255, 0.6); + font-weight: 350; + } +} + +// Disable monotile buttons on "small" devices +@include media-breakpoint-down(xxl) { + .monotile-btn { + display: none; + } } \ No newline at end of file diff --git a/app/controllers/administration_controller.rb b/app/controllers/administration_controller.rb index 05f39396f..1f5ebe60d 100644 --- a/app/controllers/administration_controller.rb +++ b/app/controllers/administration_controller.rb @@ -5,7 +5,7 @@ class AdministrationController < ApplicationController # tell cancancan there is no model for this controller, but authorize # nevertheless authorize_resource class: false - layout 'administration' + layout "administration" def current_ability @current_ability ||= AdministrationAbility.new(current_user) @@ -26,6 +26,6 @@ def classification end def search - @tags = params[:sort] == 'tag' + @tags = params[:sort] == "tag" end end diff --git a/app/controllers/announcements_controller.rb b/app/controllers/announcements_controller.rb index ba3492fff..8425893a6 100644 --- a/app/controllers/announcements_controller.rb +++ b/app/controllers/announcements_controller.rb @@ -1,6 +1,6 @@ # AnnouncementsController class AnnouncementsController < ApplicationController - layout 'administration' + layout "administration" before_action :set_announcement, except: [:new, :create, :index] authorize_resource except: [:new, :create, :index] @@ -17,7 +17,7 @@ def index end def new - @lecture = Lecture.find_by_id(params[:lecture]) + @lecture = Lecture.find_by(id: params[:lecture]) @announcement = Announcement.new(announcer: current_user, lecture: @lecture) authorize! :new, @announcement end @@ -33,14 +33,14 @@ def create # send notification email send_notification_email # redirection depending from where the announcement was created - unless @announcement.lecture.present? + if @announcement.lecture.blank? redirect_to announcements_path return end redirect_to edit_lecture_path(@announcement.lecture) return end - @errors = @announcement.errors[:details].join(', ') + @errors = @announcement.errors[:details].join(", ") end def propagate @@ -66,12 +66,12 @@ def create_notifications User end notifications = [] - users_to_notify.update_all(updated_at: Time.now) + users_to_notify.touch_all users_to_notify.find_each do |u| notifications << Notification.new(recipient: u, notifiable_id: @announcement.id, - notifiable_type: 'Announcement', - action: 'create') + notifiable_type: "Announcement", + action: "create") end # use activerecord-import gem to use only one SQL instruction Notification.import notifications @@ -86,19 +86,19 @@ def send_notification_email end I18n.available_locales.each do |l| local_recipients = recipients.where(locale: l) - if local_recipients.any? - NotificationMailer.with(recipients: local_recipients.pluck(:id), - locale: l, - announcement: @announcement) - .announcement_email.deliver_later - end + next unless local_recipients.any? + + NotificationMailer.with(recipients: local_recipients.pluck(:id), + locale: l, + announcement: @announcement) + .announcement_email.deliver_later end end def set_announcement - @announcement = Announcement.find_by_id(params[:id]) + @announcement = Announcement.find_by(id: params[:id]) return if @announcement.present? - redirect_to :root, alert: I18n.t('controllers.no_announcement') + redirect_to :root, alert: I18n.t("controllers.no_announcement") end end diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index 8e0441d4d..937273189 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -8,7 +8,7 @@ def current_ability end def new - question = Question.find_by_id(params[:question_id]) + question = Question.find_by(id: params[:question_id]) @answer = Answer.new(value: true, question: question) authorize! :new, @answer I18n.locale = question&.locale_with_inheritance @@ -36,16 +36,16 @@ def destroy def update_answer_box @answer_id = params[:answer_id].to_i - @value = params[:value] == 'true' + @value = params[:value] == "true" end private def set_answer - @answer = Answer.find_by_id(params[:id]) + @answer = Answer.find_by(id: params[:id]) return if @answer.present? - redirect_to root_path, alert: I18n.t('controllers.no_answer') + redirect_to root_path, alert: I18n.t("controllers.no_answer") end def answer_params diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a19c8553f..0a5a60683 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,12 +9,10 @@ class ApplicationController < ActionController::Base before_action :set_locale after_action :store_interaction, if: :user_signed_in? - etag { current_user.try :id } + etag { current_user.try(:id) } def current_user - unless controller_name == 'administration' && action_name == 'index' - return super - end + return super unless controller_name == "administration" && action_name == "index" @current_user ||= super.tap do |user| ::ActiveRecord::Associations::Preloader.new(records: [user], @@ -22,20 +20,20 @@ def current_user [:lectures, :edited_media, :clickers, - edited_courses: + { edited_courses: [:editors, - lectures: [:term, - :teacher]], - edited_lectures: + { lectures: [:term, + :teacher] }], + edited_lectures: [:course, :term, :teacher], - given_lectures: + given_lectures: [:course, :term, :teacher], - notifications: - [:notifiable]]).call + notifications: + [:notifiable] }]).call end end @@ -46,7 +44,7 @@ def current_user rescue_from ActionController::InvalidAuthenticityToken do redirect_to main_app.root_url, - alert: I18n.t('controllers.session_expired') + alert: I18n.t("controllers.session_expired") end # determine where to send the user after login @@ -96,18 +94,18 @@ def store_user_location! def set_locale I18n.locale = current_user.try(:locale) || locale_param || cookie_locale_param || I18n.default_locale - unless user_signed_in? - cookies[:locale] = I18n.locale - end + return if user_signed_in? + + cookies[:locale] = I18n.locale end def store_interaction - return if controller_name.in?(['sessions', 'administration', 'users', - 'events', 'interactions', 'profile', - 'clickers', 'clicker_votes', 'registrations']) - return if controller_name == 'main' && action_name == 'home' - return if controller_name == 'tags' && action_name.in?(['fill_tag_select', - 'fill_course_tags']) + return if controller_name.in?(["sessions", "administration", "users", + "events", "interactions", "profile", + "clickers", "clicker_votes", "registrations"]) + return if controller_name == "main" && action_name == "home" + return if controller_name == "tags" && action_name.in?(["fill_tag_select", + "fill_course_tags"]) study_participant = current_user.anonymized_id if current_user.study_participant # as of Rack 2.0.8, the session_id is wrapped in a class of its own @@ -115,7 +113,7 @@ def store_interaction # see https://github.com/rack/rack/issues/1433 InteractionSaver.perform_async(request.session_options[:id].public_id, request.original_fullpath, - request.referrer, + request.referer, study_participant) end diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index 9f2b97f09..803c72927 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -10,12 +10,15 @@ def current_ability def new @assignment = Assignment.new - @lecture = Lecture.find_by_id(params[:lecture_id]) + @lecture = Lecture.find_by(id: params[:lecture_id]) @assignment.lecture = @lecture authorize! :new, @assignment set_assignment_locale end + def edit + end + def create @assignment = Assignment.new(assignment_params) authorize! :create, @assignment @@ -25,9 +28,6 @@ def create set_assignment_locale end - def edit - end - def update @assignment.update(assignment_params) @errors = @assignment.errors @@ -44,7 +44,7 @@ def cancel_edit end def cancel_new - @lecture = Lecture.find_by_id(params[:lecture]) + @lecture = Lecture.find_by(id: params[:lecture]) assignment = Assignment.new(lecture: @lecture) authorize! :cancel_new, assignment set_assignment_locale @@ -54,18 +54,18 @@ def cancel_new private def set_assignment - @assignment = Assignment.find_by_id(params[:id]) + @assignment = Assignment.find_by(id: params[:id]) @lecture = @assignment&.lecture set_assignment_locale and return if @assignment - redirect_to :root, alert: I18n.t('controllers.no_assignment') + redirect_to :root, alert: I18n.t("controllers.no_assignment") end def set_lecture - @lecture = Lecture.find_by_id(assignment_params[:lecture_id]) + @lecture = Lecture.find_by(id: assignment_params[:lecture_id]) return if @lecture - redirect_to :root, alert: I18n.t('controllers.no_lecture') + redirect_to :root, alert: I18n.t("controllers.no_lecture") end def set_assignment_locale diff --git a/app/controllers/chapters_controller.rb b/app/controllers/chapters_controller.rb index 1b6188a3c..c39c976da 100644 --- a/app/controllers/chapters_controller.rb +++ b/app/controllers/chapters_controller.rb @@ -3,32 +3,22 @@ class ChaptersController < ApplicationController before_action :set_chapter, except: [:new, :create] authorize_resource except: [:new, :create] before_action :set_view_locale, only: [:edit] - layout 'administration' + layout "administration" def current_ability @current_ability ||= ChapterAbility.new(current_user) end - def edit - @section = Section.find_by_id(params[:section_id]) - end - - def update + def new + @lecture = Lecture.find_by(id: params[:lecture_id]) + @chapter = Chapter.new(lecture: @lecture) + authorize! :new, @chapter I18n.locale = @chapter.lecture.locale_with_inheritance || current_user.locale || I18n.default_locale - @chapter.update(chapter_params) - if @chapter.valid? - predecessor = params[:chapter][:predecessor] - # place the chapter in the correct position - if predecessor.present? - position = predecessor.to_i - position -= 1 if position > @chapter.position - @chapter.insert_at(position + 1) - end - redirect_to edit_chapter_path(@chapter) - return - end - @errors = @chapter.errors + end + + def edit + @section = Section.find_by(id: params[:section_id]) end def create @@ -47,12 +37,22 @@ def create @errors = @chapter.errors end - def new - @lecture = Lecture.find_by_id(params[:lecture_id]) - @chapter = Chapter.new(lecture: @lecture) - authorize! :new, @chapter + def update I18n.locale = @chapter.lecture.locale_with_inheritance || current_user.locale || I18n.default_locale + @chapter.update(chapter_params) + if @chapter.valid? + predecessor = params[:chapter][:predecessor] + # place the chapter in the correct position + if predecessor.present? + position = predecessor.to_i + position -= 1 if position > @chapter.position + @chapter.insert_at(position + 1) + end + redirect_to edit_chapter_path(@chapter) + return + end + @errors = @chapter.errors end def destroy @@ -69,10 +69,10 @@ def list_sections private def set_chapter - @chapter = Chapter.find_by_id(params[:id]) + @chapter = Chapter.find_by(id: params[:id]) return if @chapter.present? - redirect_to :root, alert: I18n.t('controllers.no_chapter') + redirect_to :root, alert: I18n.t("controllers.no_chapter") end def chapter_params diff --git a/app/controllers/clicker_votes_controller.rb b/app/controllers/clicker_votes_controller.rb index d2b684e04..5e2ae66d5 100644 --- a/app/controllers/clicker_votes_controller.rb +++ b/app/controllers/clicker_votes_controller.rb @@ -7,9 +7,7 @@ def create @clicker = @vote.clicker if cookies["clicker-#{@clicker.id}"] != @clicker.instance @vote.save - if @vote.valid? - cookies["clicker-#{@vote.clicker_id}"] = @clicker.instance - end + cookies["clicker-#{@vote.clicker_id}"] = @clicker.instance if @vote.valid? end head :ok end diff --git a/app/controllers/clickers_controller.rb b/app/controllers/clickers_controller.rb index 55ced38b8..d15b9098b 100644 --- a/app/controllers/clickers_controller.rb +++ b/app/controllers/clickers_controller.rb @@ -1,19 +1,32 @@ # ClickersController class ClickersController < ApplicationController skip_before_action :authenticate_user!, only: [:show, :edit, :open, :close, - :reset, - :get_votes_count, + :votes_count, :set_alternatives, :render_clickerizable_actions] before_action :set_clicker, except: [:new, :create] authorize_resource except: [:new, :create, :edit, :open, :close, :set_alternatives] - layout 'clicker', except: [:edit] + layout "clicker", except: [:edit] def current_ability @current_ability ||= ClickerAbility.new(current_user) end + def show + if params[:code] == @clicker.code + redirect_to edit_clicker_path(@clicker, + params: { code: @clicker.code }) + return + end + if stale?(etag: @clicker, + last_modified: [@clicker.updated_at, + Time.zone.parse(ENV.fetch("RAILS_CACHE_ID", nil))].max) + render :show + nil + end + end + def new @clicker = Clicker.new authorize! :new, @clicker @@ -23,30 +36,16 @@ def edit authorize! :edit, @clicker, @entered_code @user_path = clicker_url(@clicker, host: DefaultSetting::URL_HOST_SHORT) - .gsub('clickers', 'c') + .gsub("clickers", "c") @editor_path = clicker_url(@clicker, host: DefaultSetting::URL_HOST_SHORT, params: { code: @clicker.code }) - .gsub('clickers', 'c') + .gsub("clickers", "c") if user_signed_in? - render layout: 'administration' - return - end - render layout: 'edit_clicker' - end - - def show - if params[:code] == @clicker.code - redirect_to edit_clicker_path(@clicker, - params: { code: @clicker.code }) - return - end - if stale?(etag: @clicker, - last_modified: [@clicker.updated_at, - Time.parse(ENV['RAILS_CACHE_ID'])].max) - render :show + render layout: "administration" return end + render layout: "edit_clicker" end def create @@ -58,7 +57,7 @@ def create return end @errors = @clicker.errors - render layout: 'administration' + render layout: "administration" end def destroy @@ -69,28 +68,28 @@ def destroy def open authorize! :open, @clicker, @entered_code @clicker.open! - render layout: 'administration' if user_signed_in? + render layout: "administration" if user_signed_in? end def close authorize! :close, @clicker, @entered_code @clicker.close! - render layout: 'administration' if user_signed_in? + render layout: "administration" if user_signed_in? end def set_alternatives authorize! :set_alternatives, @clicker, @entered_code @clicker.update(alternatives: params[:alternatives].to_i) - head :ok, content_type: 'text/html' + head :ok, content_type: "text/html" end - def get_votes_count + def votes_count result = @clicker.votes.count render json: result end def associate_question - question = Question.find_by_id(clicker_params[:question_id]) + question = Question.find_by(id: clicker_params[:question_id]) @clicker.update(question: question, alternatives: question&.answers&.count || 3) redirect_to edit_clicker_path(@clicker) @@ -106,8 +105,8 @@ def remove_question def render_clickerizable_actions I18n.locale = current_user.locale - @medium = Medium.find_by_id(params[:medium_id]) - @question = Question.find_by_id(params[:medium_id]) + @medium = Medium.find_by(id: params[:medium_id]) + @question = Question.find_by(id: params[:medium_id]) end private @@ -122,11 +121,11 @@ def code_params end def set_clicker - @clicker = Clicker.find_by_id(params[:id]) + @clicker = Clicker.find_by(id: params[:id]) @code = user_signed_in? ? nil : @clicker&.code @entered_code = code_params[:code] return if @clicker - redirect_to :root, alert: I18n.t('controllers.no_clicker') + redirect_to :root, alert: I18n.t("controllers.no_clicker") end end diff --git a/app/controllers/commontator/comments_controller.rb b/app/controllers/commontator/comments_controller.rb index 7f670c7e7..31058b3da 100644 --- a/app/controllers/commontator/comments_controller.rb +++ b/app/controllers/commontator/comments_controller.rb @@ -1,222 +1,227 @@ # The CommmentsController is copied from the Commontator gem # Only minor customizations are made -class Commontator::CommentsController < Commontator::ApplicationController - before_action :set_thread, only: [:new, :create] - before_action :set_comment_and_thread, except: [:new, :create] - before_action :commontator_set_thread_variables, - only: [:show, :update, :delete, :undelete] - - # GET /comments/1 - def show - respond_to do |format| - format.html { redirect_to commontable_url } - format.js - end - end - - # GET /threads/1/comments/new - def new - @comment = Commontator::Comment.new(thread: @commontator_thread, - creator: @commontator_user) - parent_id = params.dig(:comment, :parent_id) - unless parent_id.blank? - parent = Commontator::Comment.find parent_id - @comment.parent = parent - @comment.body = "
#{ - Commontator.commontator_name(parent.creator) - }\n#{ - parent.body - }\n
\n" if [:q, - :b].include? @commontator_thread.config.comment_reply_style +module Commontator + class CommentsController < Commontator::ApplicationController + before_action :set_thread, only: [:new, :create] + before_action :set_comment_and_thread, except: [:new, :create] + before_action :commontator_set_thread_variables, + only: [:show, :update, :delete, :undelete] + + # GET /comments/1 + def show + respond_to do |format| + format.html { redirect_to commontable_url } + format.js + end end - security_transgression_unless @comment.can_be_created_by?(@commontator_user) - respond_to do |format| - format.html { redirect_to commontable_url } - format.js - end - end - - # POST /threads/1/comments - def create - @comment = Commontator::Comment.new( - thread: @commontator_thread, creator: @commontator_user, body: params.dig( - :comment, :body - ) - ) - parent_id = params.dig(:comment, :parent_id) - @comment.parent = Commontator::Comment.find(parent_id) unless parent_id.blank? - security_transgression_unless @comment.can_be_created_by?(@commontator_user) - - respond_to do |format| - if params[:cancel].blank? - if @comment.save - sub = @commontator_thread.config.thread_subscription.to_sym - @commontator_thread.subscribe(@commontator_user) if sub == :a || sub == :b - subscribe_mentioned if @commontator_thread.config.mentions_enabled - Commontator::Subscription.comment_created(@comment) - # The next line constitutes a customization of the original controller - update_unread_status - - @commontator_page = @commontator_thread.new_comment_page( - @comment.parent_id, @commontator_show_all - ) - - format.js - else - format.js { render :new } + # GET /threads/1/comments/new + def new + @comment = Commontator::Comment.new(thread: @commontator_thread, + creator: @commontator_user) + parent_id = params.dig(:comment, :parent_id) + if parent_id.present? + parent = Commontator::Comment.find(parent_id) + @comment.parent = parent + if [:q, + :b].include?(@commontator_thread.config.comment_reply_style) + @comment.body = "
#{ + Commontator.commontator_name(parent.creator) + }\n#{ + parent.body + }\n
\n" end - else - format.js { render :cancel } end + security_transgression_unless(@comment.can_be_created_by?(@commontator_user)) - format.html { redirect_to commontable_url } + respond_to do |format| + format.html { redirect_to commontable_url } + format.js + end end - end - # GET /comments/1/edit - def edit - @comment.editor = @commontator_user - security_transgression_unless @comment.can_be_edited_by?(@commontator_user) + # GET /comments/1/edit + def edit + @comment.editor = @commontator_user + security_transgression_unless(@comment.can_be_edited_by?(@commontator_user)) - respond_to do |format| - format.html { redirect_to commontable_url } - format.js + respond_to do |format| + format.html { redirect_to commontable_url } + format.js + end end - end - # PUT /comments/1 - def update - @comment.editor = @commontator_user - @comment.body = params.dig(:comment, :body) - security_transgression_unless @comment.can_be_edited_by?(@commontator_user) + # POST /threads/1/comments + def create + @comment = Commontator::Comment.new( + thread: @commontator_thread, creator: @commontator_user, body: params.dig( + :comment, :body + ) + ) + parent_id = params.dig(:comment, :parent_id) + @comment.parent = Commontator::Comment.find(parent_id) if parent_id.present? + security_transgression_unless(@comment.can_be_created_by?(@commontator_user)) + + respond_to do |format| + if params[:cancel].blank? + if @comment.save + sub = @commontator_thread.config.thread_subscription.to_sym + @commontator_thread.subscribe(@commontator_user) if [:a, :b].include?(sub) + subscribe_mentioned if @commontator_thread.config.mentions_enabled + Commontator::Subscription.comment_created(@comment) + # The next line constitutes a customization of the original controller + update_unread_status + + @commontator_page = @commontator_thread.new_comment_page( + @comment.parent_id, @commontator_show_all + ) + + format.js + else + format.js { render :new } + end + else + format.js { render :cancel } + end - respond_to do |format| - if params[:cancel].blank? - if @comment.save - subscribe_mentioned if @commontator_thread.config.mentions_enabled + format.html { redirect_to commontable_url } + end + end - format.js + # PUT /comments/1 + def update + @comment.editor = @commontator_user + @comment.body = params.dig(:comment, :body) + security_transgression_unless(@comment.can_be_edited_by?(@commontator_user)) + + respond_to do |format| + if params[:cancel].blank? + if @comment.save + subscribe_mentioned if @commontator_thread.config.mentions_enabled + + format.js + else + format.js { render :edit } + end else - format.js { render :edit } + @comment.restore_attributes + + format.js { render :cancel } end - else - @comment.restore_attributes - format.js { render :cancel } + format.html { redirect_to commontable_url } end - - format.html { redirect_to commontable_url } end - end - # PUT /comments/1/delete - def delete - security_transgression_unless @comment.can_be_deleted_by?(@commontator_user) + # PUT /comments/1/delete + def delete + security_transgression_unless(@comment.can_be_deleted_by?(@commontator_user)) - @comment.errors.add(:base, - t('commontator.comment.errors.already_deleted')) \ unless @comment.delete_by(@commontator_user) + @comment.errors.add(:base, + t("commontator.comment.errors.already_deleted")) + end - respond_to do |format| - format.html { redirect_to commontable_url } - format.js { render :delete } + respond_to do |format| + format.html { redirect_to commontable_url } + format.js { render :delete } + end end - end - # PUT /comments/1/undelete - def undelete - security_transgression_unless @comment.can_be_deleted_by?(@commontator_user) + # PUT /comments/1/undelete + def undelete + security_transgression_unless(@comment.can_be_deleted_by?(@commontator_user)) - @comment.errors.add(:base, t('commontator.comment.errors.not_deleted')) \ - unless @comment.undelete_by(@commontator_user) + @comment.errors.add(:base, t("commontator.comment.errors.not_deleted")) \ + unless @comment.undelete_by(@commontator_user) - respond_to do |format| - format.html { redirect_to commontable_url } - format.js { render :delete } + respond_to do |format| + format.html { redirect_to commontable_url } + format.js { render :delete } + end end - end - # PUT /comments/1/upvote - def upvote - security_transgression_unless @comment.can_be_voted_on_by?(@commontator_user) + # PUT /comments/1/upvote + def upvote + security_transgression_unless(@comment.can_be_voted_on_by?(@commontator_user)) - @comment.upvote_from @commontator_user + @comment.upvote_from(@commontator_user) - respond_to do |format| - format.html { redirect_to commontable_url } - format.js { render :vote } + respond_to do |format| + format.html { redirect_to commontable_url } + format.js { render :vote } + end end - end - # PUT /comments/1/downvote - def downvote - security_transgression_unless @comment.can_be_voted_on_by?(@commontator_user) && \ - @comment.thread.config.comment_voting.to_sym == :ld + # PUT /comments/1/downvote + def downvote + security_transgression_unless(@comment.can_be_voted_on_by?(@commontator_user) && \ + @comment.thread.config.comment_voting.to_sym == :ld) - @comment.downvote_from @commontator_user + @comment.downvote_from(@commontator_user) - respond_to do |format| - format.html { redirect_to commontable_url } - format.js { render :vote } + respond_to do |format| + format.html { redirect_to commontable_url } + format.js { render :vote } + end end - end - # PUT /comments/1/unvote - def unvote - security_transgression_unless @comment.can_be_voted_on_by?(@commontator_user) + # PUT /comments/1/unvote + def unvote + security_transgression_unless(@comment.can_be_voted_on_by?(@commontator_user)) - @comment.unvote voter: @commontator_user + @comment.unvote(voter: @commontator_user) - respond_to do |format| - format.html { redirect_to commontable_url } - format.js { render :vote } + respond_to do |format| + format.html { redirect_to commontable_url } + format.js { render :vote } + end end - end - protected + protected - def set_comment_and_thread - @comment = Commontator::Comment.find(params[:id]) - @commontator_thread = @comment.thread - end + def set_comment_and_thread + @comment = Commontator::Comment.find(params[:id]) + @commontator_thread = @comment.thread + end - def subscribe_mentioned - Commontator.commontator_mentions(@commontator_user, @commontator_thread, - '') - .where(id: params[:mentioned_ids]) - .each do |user| - @commontator_thread.subscribe(user) + def subscribe_mentioned + Commontator.commontator_mentions(@commontator_user, @commontator_thread, + "") + .where(id: params[:mentioned_ids]) + .find_each do |user| + @commontator_thread.subscribe(user) + end end - end - # This method ensures that the unread_comments flag is updated - # for users affected by the creation of a newly created comment - # It constitues a customization - def update_unread_status - medium = @commontator_thread.commontable - return unless medium.released.in?(['all', 'users', 'subscribers']) - - relevant_users = medium.teachable.media_scope.users - relevant_users.where.not(id: current_user.id) - .where(unread_comments: false) - .update_all(unread_comments: true) - - # make sure that the thread associated to this comment is marked as read - # by the comment creator (unless some other user posted a comment in it - # that has not yet been read) - @reader = Reader.find_or_create_by(user: current_user, - thread: @commontator_thread) - if unseen_comments? - @update_icon = true - return - end - @reader.update(updated_at: Time.current) - end + # This method ensures that the unread_comments flag is updated + # for users affected by the creation of a newly created comment + # It constitues a customization + def update_unread_status + medium = @commontator_thread.commontable + return unless medium.released.in?(["all", "users", "subscribers"]) + + relevant_users = medium.teachable.media_scope.users + relevant_users.where.not(id: current_user.id) + .where(unread_comments: false) + .update(unread_comments: true) + + # make sure that the thread associated to this comment is marked as read + # by the comment creator (unless some other user posted a comment in it + # that has not yet been read) + @reader = Reader.find_or_create_by(user: current_user, + thread: @commontator_thread) + if unseen_comments? + @update_icon = true + return + end + @reader.touch + end - def unseen_comments? - @commontator_thread.comments.any? do |c| - c.creator != current_user && c.created_at > @reader.updated_at + def unseen_comments? + @commontator_thread.comments.any? do |c| + c.creator != current_user && c.created_at > @reader.updated_at + end end - end + end end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index b0fd5e1ed..66daae203 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -1,7 +1,7 @@ class ConfirmationsController < Devise::ConfirmationsController private - def after_confirmation_path_for(resource_name, resource) + def after_confirmation_path_for(_resource_name, resource) sign_in(resource) # In case you want to sign in the user edit_profile_path end diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 67be24754..bfe9704a9 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -5,7 +5,7 @@ class CoursesController < ApplicationController before_action :check_if_enough_questions, only: [:take_random_quiz] before_action :check_for_consent authorize_resource except: [:create, :search] - layout 'administration' + layout "administration" def current_ability @current_ability ||= CourseAbility.new(current_user) @@ -15,22 +15,6 @@ def edit I18n.locale = @course.locale || I18n.default_locale end - def update - I18n.locale = @course.locale || I18n.default_locale - old_image_data = @course.image_data - @course.update(course_params) - @errors = @course.errors - return unless @errors.empty? - - @course.update(image: nil) if params[:course][:detach_image] == 'true' - changed_image = @course.image_data != old_image_data - if @course.image.present? && changed_image - @course.image_derivatives! - @course.save - end - @errors = @course.errors - end - def create @course = Course.new(course_params) authorize! :create, @course @@ -39,15 +23,31 @@ def create # set organizational_concept to default set_organizational_defaults redirect_to administration_path, - notice: I18n.t('controllers.created_course_success', + notice: I18n.t("controllers.created_course_success", course: @course.title, editors: @course.editors.map(&:name) - .join(', ')) + .join(", ")) return end @errors = @course.errors end + def update + I18n.locale = @course.locale || I18n.default_locale + old_image_data = @course.image_data + @course.update(course_params) + @errors = @course.errors + return unless @errors.empty? + + @course.update(image: nil) if params[:course][:detach_image] == "true" + changed_image = @course.image_data != old_image_data + if @course.image.present? && changed_image + @course.image_derivatives! + @course.save + end + @errors = @course.errors + end + def destroy @course.destroy # destroy all notifications related to this course @@ -80,14 +80,14 @@ def search private def set_course - @course = Course.find_by_id(params[:id]) + @course = Course.find_by(id: params[:id]) return if @course.present? - redirect_to :root, alert: I18n.t('controllers.no_course') + redirect_to :root, alert: I18n.t("controllers.no_course") end def set_course_admin - @course = Course.find_by_id(params[:id]) + @course = Course.find_by(id: params[:id]) return if @course.present? redirect_to administration_path @@ -121,15 +121,15 @@ def random_quiz_params # destroy all notifications related to this course def destroy_notifications - Notification.where(notifiable_id: @course.id, notifiable_type: 'Course') + Notification.where(notifiable_id: @course.id, notifiable_type: "Course") .delete_all end # fill organizational_concept with default view def set_organizational_defaults @course.update(organizational_concept: - render_to_string(partial: 'courses/' \ - 'organizational_default', + render_to_string(partial: "courses/" \ + "organizational_default", formats: :html, layout: false)) end @@ -137,7 +137,7 @@ def set_organizational_defaults def check_if_enough_questions return if @course.enough_questions? - redirect_to :root, alert: I18n.t('controllers.no_test') + redirect_to :root, alert: I18n.t("controllers.no_test") end def check_for_consent diff --git a/app/controllers/divisions_controller.rb b/app/controllers/divisions_controller.rb index 9c8ecdd53..dc2195d75 100644 --- a/app/controllers/divisions_controller.rb +++ b/app/controllers/divisions_controller.rb @@ -7,17 +7,12 @@ def current_ability @current_ability ||= DivisionAbility.new(current_user) end - def edit - end - def new @division = Division.new(program_id: params[:program_id].to_i) authorize! :new, @division end - def update - @division.update(division_params) - redirect_to classification_path + def edit end def create @@ -28,6 +23,11 @@ def create redirect_to classification_path end + def update + @division.update(division_params) + redirect_to classification_path + end + def destroy @division.destroy redirect_to classification_path @@ -36,10 +36,10 @@ def destroy private def set_division - @division = Division.find_by_id(params[:id]) + @division = Division.find_by(id: params[:id]) return if @division.present? - redirect_to root_path, alert: I18n.t('controllers.no_division') + redirect_to root_path, alert: I18n.t("controllers.no_division") end def division_params diff --git a/app/controllers/erdbeere_controller.rb b/app/controllers/erdbeere_controller.rb index d880b9114..23c78601e 100644 --- a/app/controllers/erdbeere_controller.rb +++ b/app/controllers/erdbeere_controller.rb @@ -1,38 +1,38 @@ # ExamplesController class ErdbeereController < ApplicationController authorize_resource class: false - layout 'application' + layout "application" def current_ability @current_ability ||= ErdbeereAbility.new(current_user) end def show_example - response = Faraday.get(ENV['ERDBEERE_API'] + "/examples/#{params[:id]}") + response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + "/examples/#{params[:id]}") @content = if response.status == 200 - JSON.parse(response.body)['embedded_html'] + JSON.parse(response.body)["embedded_html"] else - 'Something went wrong.' + "Something went wrong." end end def show_property - response = Faraday.get(ENV['ERDBEERE_API'] + "/properties/#{params[:id]}") + response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + "/properties/#{params[:id]}") @content = if response.status == 200 - JSON.parse(response.body)['embedded_html'] + JSON.parse(response.body)["embedded_html"] else - 'Something went wrong.' + "Something went wrong." end end def show_structure - id = params[:id] - response = Faraday.get(ENV['ERDBEERE_API'] + "/structures/#{params[:id]}") + params[:id] + response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + "/structures/#{params[:id]}") @content = if response.status == 200 - JSON.parse(response.body)['embedded_html'] + JSON.parse(response.body)["embedded_html"] else - 'Something went wrong.' + "Something went wrong." end end @@ -51,18 +51,18 @@ def cancel_edit_tags def display_info @id = params[:id] @sort = params[:sort] - response = Faraday.get(ENV['ERDBEERE_API'] + + response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + "/#{@sort.downcase.pluralize}/#{@id}/view_info") @content = JSON.parse(response.body) if response.status != 200 - @info = 'Something went wrong' + @info = "Something went wrong" return end - @info = if @sort == 'Structure' - @content['data']['attributes']['name'] + @info = if @sort == "Structure" + @content["data"]["attributes"]["name"] else - "#{@content['included'][0]['attributes']['name']}:"\ - "#{@content['data']['attributes']['name']}" + "#{@content["included"][0]["attributes"]["name"]}:" \ + "#{@content["data"]["attributes"]["name"]}" end end @@ -79,7 +79,7 @@ def update_tags removed_tags.each do |t| t.update(realizations: t.realizations - [[sort, id]]) end - if sort == 'Structure' + if sort == "Structure" redirect_to erdbeere_structure_path(id) return end @@ -87,28 +87,26 @@ def update_tags end def fill_realizations_select - response = Faraday.get(ENV['ERDBEERE_API'] + '/structures/') - @tag = Tag.find_by_id(params[:id]) + response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/structures/") + @tag = Tag.find_by(id: params[:id]) hash = JSON.parse(response.body) - @structures = hash['data'].map do |d| - { id: d['id'], - name: d['attributes']['name'], - properties: d['relationships']['original_properties']['data'].map { |x| - x['id'] - } } + @structures = hash["data"].map do |d| + { id: d["id"], + name: d["attributes"]["name"], + properties: d["relationships"]["original_properties"]["data"].pluck("id") } end - @properties = hash['included'].map do |d| - { id: d['id'], - name: d['attributes']['name'] } + @properties = hash["included"].map do |d| + { id: d["id"], + name: d["attributes"]["name"] } end end def find_example - response = Faraday.get(ENV['ERDBEERE_API'] + '/find?' + find_params.to_query) + response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/find?#{find_params.to_query}") @content = if response.status == 200 - JSON.parse(response.body)['embedded_html'] + JSON.parse(response.body)["embedded_html"] else - 'Something went wrong.' + "Something went wrong." end end diff --git a/app/controllers/interactions_controller.rb b/app/controllers/interactions_controller.rb index 81242996b..20cb9c5fd 100644 --- a/app/controllers/interactions_controller.rb +++ b/app/controllers/interactions_controller.rb @@ -1,7 +1,7 @@ # InteractionsController class InteractionsController < ApplicationController authorize_resource - layout 'administration' + layout "administration" def index end @@ -12,10 +12,10 @@ def export_interactions @interactions = Interaction.created_between(start_date, end_date) respond_to do |format| format.html { head :ok } - format.csv { - send_data @interactions.to_csv, - filename: "interactions-from-#{start_date}-to-#{end_date}-at-#{Time.now}.csv" - } + format.csv do + csv_filename = "interactions-from-#{start_date}-to-#{end_date}-at-#{Time.zone.now}.csv" + send_data(@interactions.to_csv, filename: csv_filename) + end end end @@ -25,10 +25,10 @@ def export_probes @probes = Probe.created_between(start_date, end_date) respond_to do |format| format.html { head :ok } - format.csv { - send_data @probes.to_csv, - filename: "probes-from-#{start_date}-to-#{end_date}-at-#{Time.now}.csv" - } + format.csv do + send_data(@probes.to_csv, + filename: "probes-from-#{start_date}-to-#{end_date}-at-#{Time.zone.now}.csv") + end end end diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb index 56c0bf885..648068202 100644 --- a/app/controllers/items_controller.rb +++ b/app/controllers/items_controller.rb @@ -7,12 +7,6 @@ def current_ability @current_ability ||= ItemAbility.new(current_user) end - def update - I18n.locale = @item.medium.locale_with_inheritance if @item.medium - @item.update(item_params) - @errors = @item.errors unless @item.valid? - end - def edit I18n.locale = @item.medium.locale_with_inheritance if @item.medium end @@ -34,10 +28,16 @@ def create render :update end + def update + I18n.locale = @item.medium.locale_with_inheritance if @item.medium + @item.update(item_params) + @errors = @item.errors unless @item.valid? + end + def destroy @medium = @item.medium @item.destroy - redirect_to edit_medium_path(@medium) if params[:from] == 'quarantine' + redirect_to edit_medium_path(@medium) if params[:from] == "quarantine" end # if an item is selected from within the reference editor in thyme, @@ -55,9 +55,7 @@ def set_item end def set_explanation - if @referral_id.zero? || @item != Referral.find(@referral_id).item - return @item.explanation - end + return @item.explanation if @referral_id.zero? || @item != Referral.find(@referral_id).item Referral.find(@referral_id).explanation end @@ -71,7 +69,7 @@ def item_params if filter[:medium_id].present? filter[:start_time] = TimeStamp.new(time_string: filter[:start_time]) end - filter[:section_id] = nil if filter[:section_id] == '' + filter[:section_id] = nil if filter[:section_id] == "" filter end end diff --git a/app/controllers/lectures_controller.rb b/app/controllers/lectures_controller.rb index b2cf71332..d108fbbc0 100644 --- a/app/controllers/lectures_controller.rb +++ b/app/controllers/lectures_controller.rb @@ -11,58 +11,18 @@ class LecturesController < ApplicationController before_action :set_view_locale, only: [:edit, :show, :subscribe_page, :show_random_quizzes] before_action :check_if_enough_questions, only: [:show_random_quizzes] - layout 'administration' + layout "administration" def current_ability @current_ability ||= LectureAbility.new(current_user) end - def edit - if stale?(etag: @lecture, - last_modified: [current_user.updated_at, @lecture.updated_at, - Time.parse(ENV['RAILS_CACHE_ID'])].max) - eager_load_stuff - end - end - - def update - editor_ids = lecture_params[:editor_ids] - if editor_ids != nil - # removes the empty String "" in the NEW array of editor ids - # and converts it into an array of integers - all_ids = editor_ids.map(&:to_i) - [0] - old_ids = @lecture.editor_ids - new_ids = all_ids - old_ids - - # returns an array of Users that match the given ids - recipients = User.where(id: new_ids) - - recipients.each do |r| - NotificationMailer.with(recipient: r, - locale: r.locale, - lecture: @lecture) - .new_editor_email.deliver_later - end - end - - @lecture.update(lecture_params) - if structure_params.present? - structure_ids = structure_params.select { |_k, v| v.to_i == 1 }.keys - .map(&:to_i) - @lecture.update(structure_ids: structure_ids) - end - @lecture.touch - @lecture.forum&.update(name: @lecture.forum_title) - redirect_to edit_lecture_path(@lecture) if @lecture.valid? - @errors = @lecture.errors - end - def show # deactivate http caching for the moment if stale?(etag: @lecture, last_modified: [current_user.updated_at, @lecture.updated_at, - Time.parse(ENV['RAILS_CACHE_ID']), + Time.zone.parse(ENV.fetch("RAILS_CACHE_ID", nil)), Thredded::UserDetail.find_by(user_id: current_user.id) &.last_seen_at || @lecture.updated_at, @lecture.forum&.updated_at || @lecture.updated_at].max) @@ -72,14 +32,14 @@ def show media: [:teachable, :tags], lessons: [media: [:tags]], chapters: [:lecture, - sections: [lessons: [:tags], - chapter: [:lecture], - tags: [:notions, - :lessons]]]) - .find_by_id(params[:id]) + { sections: [lessons: [:tags], + chapter: [:lecture], + tags: [:notions, + :lessons]] }]) + .find_by(id: params[:id]) @notifications = current_user.active_notifications(@lecture) @new_topics_count = @lecture.unread_forum_topics_count(current_user) || 0 - render layout: 'application' + render layout: "application" end end @@ -87,45 +47,85 @@ def new @lecture = Lecture.new authorize! :new, @lecture @from = params[:from] - return unless @from == 'course' + return unless @from == "course" # if new action was triggered from inside a course view, add the course # info to the lecture - @lecture.course = Course.find_by_id(params[:course]) + @lecture.course = Course.find_by(id: params[:course]) I18n.locale = @lecture.course.locale end + def edit + if stale?(etag: @lecture, + last_modified: [current_user.updated_at, @lecture.updated_at, + Time.zone.parse(ENV.fetch("RAILS_CACHE_ID", nil))].max) + eager_load_stuff + end + end + def create @lecture = Lecture.new(lecture_params) authorize! :create, @lecture @lecture.save if @lecture.valid? - @lecture.update(sort: 'special') if @lecture.course.term_independent + @lecture.update(sort: "special") if @lecture.course.term_independent # set organizational_concept to default set_organizational_defaults # set lenguage to default language set_language # depending on where the create action was trriggered from, return # to admin index view or edit course view - unless params[:lecture][:from] == 'course' + unless params[:lecture][:from] == "course" redirect_to administration_path, - notice: I18n.t('controllers.created_lecture_success', + notice: I18n.t("controllers.created_lecture_success", lecture: @lecture.title_with_teacher) return end redirect_to edit_course_path(@lecture.course), - notice: I18n.t('controllers.created_lecture_success', + notice: I18n.t("controllers.created_lecture_success", lecture: @lecture.title_with_teacher) return end @errors = @lecture.errors end + def update + editor_ids = lecture_params[:editor_ids] + unless editor_ids.nil? + # removes the empty String "" in the NEW array of editor ids + # and converts it into an array of integers + all_ids = editor_ids.map(&:to_i) - [0] + old_ids = @lecture.editor_ids + new_ids = all_ids - old_ids + + # returns an array of Users that match the given ids + recipients = User.where(id: new_ids) + + recipients.each do |r| + NotificationMailer.with(recipient: r, + locale: r.locale, + lecture: @lecture) + .new_editor_email.deliver_later + end + end + + @lecture.update(lecture_params) + if structure_params.present? + structure_ids = structure_params.select { |_k, v| v.to_i == 1 }.keys + .map(&:to_i) + @lecture.update(structure_ids: structure_ids) + end + @lecture.touch + @lecture.forum&.update(name: @lecture.forum_title) + redirect_to edit_lecture_path(@lecture) if @lecture.valid? + @errors = @lecture.errors + end + def publish - @lecture.update(released: 'all') - if params[:medium][:publish_media] == '1' + @lecture.update(released: "all") + if params[:medium][:publish_media] == "1" @lecture.media_with_inheritance - .update_all(released: params[:medium][:released]) + .update(released: params[:medium][:released]) end # create notifications about creation od this lecture and send email create_notifications @@ -177,12 +177,12 @@ def show_announcements @active_notification_count = current_user.active_notifications(@lecture) .size I18n.locale = @lecture.locale_with_inheritance - render layout: 'application' + render layout: "application" end def organizational I18n.locale = @lecture.locale_with_inheritance - render layout: 'application' + render layout: "application" end def import_media @@ -195,9 +195,9 @@ def import_media end def remove_imported_medium - @medium = Medium.find_by_id(params[:medium]) + @medium = Medium.find_by(id: params[:medium]) import = Import.find_by(teachable: @lecture, medium: @medium) - import.destroy if import + import&.destroy @lecture.reload @lecture.touch end @@ -208,24 +208,26 @@ def show_subscribers end def show_structures - render layout: 'application' + render layout: "application" end def edit_structures - render layout: 'application' + render layout: "application" end def search_examples if @lecture.structure_ids.any? - response = Faraday.get(ENV['ERDBEERE_API'] + '/search') - @form = JSON.parse(response.body)['embedded_html'] - @form.gsub!('token_placeholder', - '') + # rubocop:enable Style/StringConcatenation else - @form = I18n.t('erdbeere.no_structures') + @form = I18n.t("erdbeere.no_structures") end - render layout: 'application' + render layout: "application" end def close_comments @@ -246,7 +248,7 @@ def search @total = search.total @lectures = Kaminari.paginate_array(results, total_count: @total) .page(params[:page]).per(search_params[:per]) - @results_as_list = search_params[:results_as_list] == 'true' + @results_as_list = search_params[:results_as_list] == "true" return unless @total.zero? return unless search_params[:fulltext]&.length.to_i > 1 @@ -255,24 +257,24 @@ def search def show_random_quizzes @course = @lecture.course - render layout: 'application' + render layout: "application" end def display_course @course = @lecture.course I18n.locale = @course.locale || @lecture.locale - render layout: 'application' + render layout: "application" end def subscribe_page - render layout: 'application_no_sidebar' + render layout: "application_no_sidebar" end def import_toc imported_lecture = Lecture - .find_by_id(import_toc_params[:imported_lecture_id]) - import_sections = import_toc_params[:import_sections] == '1' - import_tags = import_toc_params[:import_tags] == '1' + .find_by(id: import_toc_params[:imported_lecture_id]) + import_sections = import_toc_params[:import_sections] == "1" + import_tags = import_toc_params[:import_tags] == "1" @lecture.import_toc!(imported_lecture, import_sections, import_tags) redirect_to edit_lecture_path(@lecture) end @@ -280,10 +282,10 @@ def import_toc private def set_lecture - @lecture = Lecture.find_by_id(params[:id]) + @lecture = Lecture.find_by(id: params[:id]) return if @lecture - redirect_to :root, alert: I18n.t('controllers.no_lecture') + redirect_to :root, alert: I18n.t("controllers.no_lecture") end def set_lecture_cookie @@ -300,7 +302,9 @@ def check_for_consent end def check_for_subscribe - redirect_to subscribe_lecture_page_path(@lecture.id) unless @lecture.in?(current_user.lectures) + return if @lecture.in?(current_user.lectures) + + redirect_to subscribe_lecture_page_path(@lecture.id) end def lecture_params @@ -310,12 +314,10 @@ def lecture_params :organizational_on_top, :disable_teacher_display, :content_mode, :passphrase, :sort, :comments_disabled, :submission_max_team_size, :submission_grace_period] - if action_name == 'update' && current_user.can_update_personell?(@lecture) - allowed_params.concat([:teacher_id, editor_ids: []]) - end - if action_name == 'create' - allowed_params.concat([:course_id, :teacher_id, editor_ids: []]) + if action_name == "update" && current_user.can_update_personell?(@lecture) + allowed_params.push(:teacher_id, { editor_ids: [] }) end + allowed_params.push(:course_id, :teacher_id, { editor_ids: [] }) if action_name == "create" params.require(:lecture).permit(allowed_params) end @@ -337,8 +339,8 @@ def create_notifications User.find_each do |u| notifications << Notification.new(recipient: u, notifiable_id: @lecture.id, - notifiable_type: 'Lecture', - action: 'create') + notifiable_type: "Lecture", + action: "create") end Notification.import notifications end @@ -347,25 +349,25 @@ def send_notification_email recipients = User.where(email_for_teachable: true) I18n.available_locales.each do |l| local_recipients = recipients.where(locale: l) - if local_recipients.any? - NotificationMailer.with(recipients: local_recipients.pluck(:id), - locale: l, - lecture: @lecture) - .new_lecture_email.deliver_later - end + next unless local_recipients.any? + + NotificationMailer.with(recipients: local_recipients.pluck(:id), + locale: l, + lecture: @lecture) + .new_lecture_email.deliver_later end end # destroy all notifications related to this lecture def destroy_notifications - Notification.where(notifiable_id: @lecture.id, notifiable_type: 'Lecture') + Notification.where(notifiable_id: @lecture.id, notifiable_type: "Lecture") .delete_all end # fill organizational_concept with default view def set_organizational_defaults - partial_path = 'lectures/organizational/' - partial_path += @lecture.seminar? ? 'seminar' : 'lecture' + partial_path = "lectures/organizational/" + partial_path += @lecture.seminar? ? "seminar" : "lecture" @lecture.update(organizational_concept: render_to_string(partial: partial_path, formats: :html, @@ -384,11 +386,11 @@ def eager_load_stuff media: [:teachable, :tags], lessons: [media: [:tags]], chapters: [:lecture, - sections: [lessons: [:tags], - chapter: [:lecture], - tags: [:notions, - :lessons]]]) - .find_by_id(params[:id]) + { sections: [lessons: [:tags], + chapter: [:lecture], + tags: [:notions, + :lessons]] }]) + .find_by(id: params[:id]) @media = @lecture.media_with_inheritance_uncached_eagerload_stuff lecture_tags = @lecture.tags @course_tags = @lecture.course_tags(lecture_tags: lecture_tags) @@ -400,23 +402,23 @@ def eager_load_stuff def set_erdbeere_data @structure_ids = @lecture.structure_ids - response = Faraday.get(ENV['ERDBEERE_API'] + '/structures') + response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/structures") response_hash = if response.status == 200 JSON.parse(response.body) else - { 'data' => {}, 'included' => {} } + { "data" => {}, "included" => {} } end - @all_structures = response_hash['data'] + @all_structures = response_hash["data"] @structures = @all_structures.select do |s| - s['id'].to_i.in?(@structure_ids) + s["id"].to_i.in?(@structure_ids) end - @properties = response_hash['included'] + @properties = response_hash["included"] end def search_params types = params[:search][:types] - types = [types] if types && !types.kind_of?(Array) - types -= [''] if types + types = [types] if types && !types.is_a?(Array) + types -= [""] if types types = nil if types == [] params[:search][:types] = types params[:search][:user_id] = current_user.id @@ -432,6 +434,6 @@ def search_params def check_if_enough_questions return if @lecture.course.enough_questions? - redirect_to :root, alert: I18n.t('controllers.no_test') + redirect_to :root, alert: I18n.t("controllers.no_test") end end diff --git a/app/controllers/lessons_controller.rb b/app/controllers/lessons_controller.rb index 2425dce7d..bd8ca7e4f 100644 --- a/app/controllers/lessons_controller.rb +++ b/app/controllers/lessons_controller.rb @@ -2,7 +2,7 @@ class LessonsController < ApplicationController before_action :set_lesson, except: [:new, :create] authorize_resource except: [:new, :create] - layout 'administration' + layout "administration" def current_ability @current_ability ||= LessonAbility.new(current_user) @@ -10,22 +10,22 @@ def current_ability def show I18n.locale = @lesson.locale_with_inheritance - render layout: 'application_no_sidebar' - end - - def edit - I18n.locale = @lesson.locale_with_inheritance + render layout: "application_no_sidebar" end def new - @lecture = Lecture.find_by_id(params[:lecture_id]) + @lecture = Lecture.find_by(id: params[:lecture_id]) I18n.locale = @lecture.locale_with_inheritance if @lecture @lesson = Lesson.new(lecture: @lecture) - section = Section.find_by_id(params[:section_id]) + section = Section.find_by(id: params[:section_id]) @lesson.sections << section if section authorize! :new, @lesson end + def edit + I18n.locale = @lesson.locale_with_inheritance + end + def create @lesson = Lesson.new(lesson_params) authorize! :create, @lesson @@ -34,7 +34,7 @@ def create @lesson.tags = @lesson.sections.map(&:tags).flatten @lesson.save @errors = @lesson.errors - if @lesson.valid? && params[:commit] == t('buttons.save_and_edit') + if @lesson.valid? && params[:commit] == t("buttons.save_and_edit") redirect_to edit_lesson_path(@lesson) return end @@ -45,7 +45,7 @@ def update I18n.locale = @lesson.lecture.locale_with_inheritance @lesson.update(lesson_params) @errors = @lesson.errors - return unless @errors.blank? + return if @errors.present? update_media_order if params[:lesson][:media_order] @tags_without_section = @lesson.tags_without_section @@ -63,7 +63,7 @@ def destroy media.each do |m| m.update(teachable: lecture, description: m.description.presence || - (m.title + ' (' + I18n.t('admin.lesson.destroyed') + ')')) + "#{m.title} (#{I18n.t("admin.lesson.destroyed")})") end @lesson.destroy redirect_to edit_lecture_path(lecture) @@ -72,10 +72,10 @@ def destroy private def set_lesson - @lesson = Lesson.find_by_id(params[:id]) + @lesson = Lesson.find_by(id: params[:id]) return if @lesson.present? - redirect_to :root, alert: I18n.t('controllers.no_lesson') + redirect_to :root, alert: I18n.t("controllers.no_lesson") end def lesson_params diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 490b5103c..8fa45237c 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -2,7 +2,7 @@ class MainController < ApplicationController before_action :check_for_consent authorize_resource class: false, only: :start - layout 'application_no_sidebar' + layout "application_no_sidebar" def current_ability @current_ability ||= MainAbility.new(current_user) @@ -10,11 +10,11 @@ def current_ability def home cookies[:locale] = current_user.locale if user_signed_in? - get_announcements + announcements end def error - redirect_to :root, alert: I18n.t('controllers.no_page') + redirect_to :root, alert: I18n.t("controllers.no_page") end def news @@ -29,7 +29,7 @@ def comments @media_comments = current_user.media_latest_comments @media_comments.select! do |m| (Reader.find_by(user: current_user, thread: m[:thread]) - &.updated_at || (Time.now - 1000.years)) < m[:latest_comment].created_at && + &.updated_at || 1000.years.ago) < m[:latest_comment].created_at && m[:medium].visible_for_user?(current_user) end @media_array = Kaminari.paginate_array(@media_comments) @@ -43,7 +43,7 @@ def start :term) .sort end - get_announcements + announcements @talks = current_user.talks.includes(lecture: :term) .select { |t| t.visible_for_user?(current_user) } .sort_by do |t| @@ -60,7 +60,7 @@ def check_for_consent redirect_to consent_profile_path unless current_user.consents end - def get_announcements + def announcements @announcements = Announcement.where(on_main_page: true, lecture: nil) .pluck(:details) .join('
') diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 1c9633596..cfb78dd6e 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -21,7 +21,7 @@ class MediaController < ApplicationController :fill_medium_preview, :render_medium_actions, :render_import_media, :render_import_vertex, :cancel_import_media, :cancel_import_vertex] - layout 'administration' + layout "administration" def current_ability @current_ability ||= MediumAbility.new(current_user) @@ -30,16 +30,16 @@ def current_ability def index authorize! :index, Medium.new @media = paginated_results - render layout: 'application' + render layout: "application" end def show # destroy the notifications related to the medium - current_user.notifications.where(notifiable_type: 'Medium', - notifiable_id: @medium.id).each(&:destroy) + current_user.notifications.where(notifiable_type: "Medium", + notifiable_id: @medium.id).find_each(&:destroy) I18n.locale = @medium.locale_with_inheritance commontator_thread_show(@medium) - render layout: 'application_no_sidebar' + render layout: "application_no_sidebar" end def new @@ -48,7 +48,7 @@ def new level: 1, locale: @teachable.locale_with_inheritance) I18n.locale = @teachable.locale_with_inheritance - @medium.sort = params[:sort] ? params[:sort] : 'Kaviar' + @medium.sort = params[:sort] || "Kaviar" end def edit @@ -57,6 +57,46 @@ def edit render layout: current_user.layout end + def create + @medium = Medium.new(medium_params) + @medium.locale = @medium.teachable&.locale + @medium.editors = [current_user] + @medium.tags = @medium.teachable.tags if @medium.teachable.instance_of?(::Lesson) + authorize! :create, @medium + @medium.save + if @medium.valid? + if @medium.sort == "Remark" + @medium.update(type: "Remark", + text: I18n.t("admin.remark.initial_text")) + end + if @medium.sort == "Question" + solution = Solution.new(MampfExpression.trivial_instance) + @medium.update(type: "Question", + text: I18n.t("admin.question.initial_text"), + level: 1, + independent: false, + solution: solution, + question_sort: "mc") + Answer.create(question: @medium.becomes(Question), + text: "0", + value: true) + end + if @medium.sort == "Quiz" + @medium.update(type: "Quiz") + @medium.update(quiz_graph: QuizGraph.new(vertices: {}, + edges: {}, + root: 0, + default_table: {}, + hide_solution: []), + level: 1) + end + redirect_to edit_medium_path(@medium) + return + end + @errors = @medium.errors + render :update + end + def update I18n.locale = @medium.locale_with_inheritance old_manuscript_data = @medium.manuscript_data @@ -71,7 +111,7 @@ def update # update the associated tags), causing trouble for caching) @medium.touch # touch lectures that import this medium - @medium.importing_lectures.update_all(updated_at: Time.now) + @medium.importing_lectures.touch_all @medium.sanitize_type! # detach components if this was chosen by the user detach_components @@ -94,7 +134,7 @@ def update # refreshed_video = @medium.video # @medium.update(video_data: refreshed_video.to_json) end - if @medium.sort == 'Quiz' && params[:medium][:create_quiz_graph] == '1' + if @medium.sort == "Quiz" && params[:medium][:create_quiz_graph] == "1" @medium.becomes(Quiz).update(level: 1, quiz_graph: QuizGraph.new(vertices: {}, edges: {}, @@ -106,7 +146,7 @@ def update # remove items that correspond to named destinations that no longer # exist in the manuscript, but keep those that are referenced # from other places - if @medium.sort == 'Script' && changed_manuscript + if @medium.sort == "Script" && changed_manuscript @medium.update(imported_manuscript: false) @quarantine_added = @medium.update_pdf_destinations! if @quarantine_added.any? @@ -123,53 +163,11 @@ def update end end @tags_without_section = [] - return unless @medium.teachable.class.to_s == 'Lesson' + return unless @medium.teachable.instance_of?(::Lesson) add_tags_in_lesson_and_sections end - def create - @medium = Medium.new(medium_params) - @medium.locale = @medium.teachable&.locale - @medium.editors = [current_user] - if @medium.teachable.class.to_s == 'Lesson' - @medium.tags = @medium.teachable.tags - end - authorize! :create, @medium - @medium.save - if @medium.valid? - if @medium.sort == 'Remark' - @medium.update(type: 'Remark', - text: I18n.t('admin.remark.initial_text')) - end - if @medium.sort == 'Question' - solution = Solution.new(MampfExpression.trivial_instance) - @medium.update(type: 'Question', - text: I18n.t('admin.question.initial_text'), - level: 1, - independent: false, - solution: solution, - question_sort: 'mc') - Answer.create(question: @medium.becomes(Question), - text: '0', - value: true) - end - if @medium.sort == 'Quiz' - @medium.update(type: 'Quiz') - @medium.update(quiz_graph: QuizGraph.new(vertices: {}, - edges: {}, - root: 0, - default_table: {}, - hide_solution: []), - level: 1) - end - redirect_to edit_medium_path(@medium) - return - end - @errors = @medium.errors - render :update - end - def publish publisher = MediumPublisher.parse(@medium, current_user, publish_params) @errors = publisher.errors @@ -189,15 +187,15 @@ def destroy # destroy all notifications related to this medium destroy_notifications @medium.teachable.touch - if @medium.teachable_type == 'Lecture' + if @medium.teachable_type == "Lecture" redirect_to edit_lecture_path(@medium.teachable) return end - if @medium.teachable_type == 'Lesson' + if @medium.teachable_type == "Lesson" redirect_to edit_lesson_path(@medium.teachable) return end - if @medium.teachable_type == 'Talk' + if @medium.teachable_type == "Talk" if current_user.in?(@medium.teachable.speakers) redirect_to assemble_talk_path(@medium.teachable) return @@ -218,10 +216,12 @@ def search # get all media, then set them to only those that are visible to the current user if !current_user.active_teachable_editor? || search_params[:access].blank? filter_media = true - params["search"]["access"] = 'irrelevant' + params["search"]["access"] = "irrelevant" + end + if search_params[:answers_count].blank? + params["search"]["answers_count"] = + "irrelevant" end - params["search"]["answers_count"] = - 'irrelevant' if search_params[:answers_count].blank? search = Medium.search_by(search_params, params[:page]) search.execute @@ -231,14 +231,16 @@ def search # in the case of a search with tag_operator 'or', we # execute two searches and merge the results, where media # with the selected tags are now shown at the front of the list - if search_params[:tag_operator] == "or" and search_params[:all_tags] == "0" and search_params[:fulltext].size >= 2 - params["search"]["all_tags"] = '1' + if (search_params[:tag_operator] == "or") \ + && (search_params[:all_tags] == "0") \ + && (search_params[:fulltext].size >= 2) + params["search"]["all_tags"] = "1" search_no_tags = Medium.search_by(search_params, params[:page]) search_no_tags.execute results_no_tags = search_no_tags.results results = (results + results_no_tags).uniq @total = results.size - params["search"]["all_tags"] = '0' + params["search"]["all_tags"] = "0" end if filter_media @@ -251,40 +253,41 @@ def search @media = Kaminari.paginate_array(results, total_count: @total) .page(params[:page]).per(search_params[:per]) @purpose = search_params[:purpose] - @results_as_list = search_params[:results_as_list] == 'true' - if @purpose.in?(['quiz', 'import']) + @results_as_list = search_params[:results_as_list] == "true" + if @purpose.in?(["quiz", "import"]) render template: "media/catalog/import_preview" return end return unless @total.zero? - return unless search_params[:fulltext]&.length.to_i > 1 + + nil unless search_params[:fulltext]&.length.to_i > 1 end # play the video using thyme player def play if @medium.video.nil? - redirect_to :root, alert: I18n.t('controllers.no_video') + redirect_to :root, alert: I18n.t("controllers.no_video") return end I18n.locale = @medium.locale_with_inheritance @vtt_container = @medium.create_vtt_container! @time = params[:time] - render layout: 'thyme' + render layout: "thyme" end # show the pdf, optionally at specified page or named destination def display if @medium.manuscript.nil? - redirect_to :root, alert: I18n.t('controllers.no_manuscript') + redirect_to :root, alert: I18n.t("controllers.no_manuscript") return end if params[:destination].present? - redirect_to @medium.manuscript_url_with_host + '#' + - params[:destination].to_s, allow_other_host: true + redirect_to "#{@medium.manuscript_url_with_host}##{params[:destination]}", + allow_other_host: true return elsif params[:page].present? - redirect_to @medium.manuscript_url_with_host + '#page=' + - params[:page].to_s, allow_other_host: true + redirect_to "#{@medium.manuscript_url_with_host}#page=#{params[:page]}", + allow_other_host: true return end redirect_to @medium.manuscript_url_with_host, @@ -294,11 +297,11 @@ def display # run the geogebra applet using Geogebra's Javascript API def geogebra if @medium.geogebra.nil? - redirect_to :root, alert: I18n.t('controllers.no_geogebra') + redirect_to :root, alert: I18n.t("controllers.no_geogebra") return end I18n.locale = @medium.locale_with_inheritance - render layout: 'geogebra' + render layout: "geogebra" end # add a toc item for the video @@ -307,8 +310,8 @@ def add_item @time = params[:time].to_f @item = Item.new(medium: @medium, start_time: TimeStamp.new(total_seconds: @time)) - if @medium.sort == 'Kaviar' && - @medium.teachable_type.in?(['Lesson', 'Lecture']) + if @medium.sort == "Kaviar" && + @medium.teachable_type.in?(["Lesson", "Lecture"]) @item.section = @medium.teachable&.sections&.first end end @@ -322,15 +325,13 @@ def add_reference start_time: TimeStamp.new(total_seconds: @time), end_time: TimeStamp.new(total_seconds: @end_time)) @item_selection = @medium.teachable.media_scope.media_items_with_inheritance - @item = Item.new(sort: 'link') + @item = Item.new(sort: "link") end # add a screenshot for the video def add_screenshot - tempfile = Tempfile.new(['screenshot', '.png']) - File.open(tempfile, 'wb') do |f| - f.write params[:image].read - end + tempfile = Tempfile.new(["screenshot", ".png"]) + File.binwrite(tempfile, params[:image].read) @medium.screenshot = File.open(tempfile) @medium.save if @medium.valid? @@ -352,7 +353,7 @@ def remove_screenshot # start the thyme editor def enrich I18n.locale = @medium.locale_with_inheritance - render layout: 'enrich' + render layout: "enrich" end # if the medium is associated to a lesson of a lecture which is in script mode @@ -392,62 +393,70 @@ def fill_media_select end def update_tags - if current_user.admin || @medium.edited_with_inheritance_by?(current_user) - @medium.tags = Tag.where(id: params[:tag_ids]) - @medium.update(updated_at: Time.now) - end + return unless current_user.admin || @medium.edited_with_inheritance_by?(current_user) + + @medium.tags = Tag.where(id: params[:tag_ids]) + @medium.touch end def register_download head :ok end - def get_statistics + def statistics I18n.locale = @medium.locale || I18n.default_locale medium_consumption = Consumption.where(medium_id: @medium.id) if @medium.video.present? - @video_downloads = medium_consumption.where(sort: 'video', - mode: 'download').pluck(:created_at).map(&:to_date).tally.map { |k, t| + @video_downloads = medium_consumption + .where(sort: "video", mode: "download") + .pluck(:created_at) + .map(&:to_date).tally.map do |k, t| { x: k, y: t } - }.to_json - @video_downloads_count = medium_consumption.where(sort: 'video', - mode: 'download').count - @video_thyme = medium_consumption.where(sort: 'video', - mode: 'thyme').pluck(:created_at).map(&:to_date).tally.map { |k, t| + end.to_json + @video_downloads_count = medium_consumption.where(sort: "video", + mode: "download").count + @video_thyme = medium_consumption + .where(sort: "video", mode: "thyme") + .pluck(:created_at) + .map(&:to_date).tally.map do |k, t| { x: k, y: t } - }.to_json - @video_thyme_count = medium_consumption.where(sort: 'video', - mode: 'thyme').count + end.to_json + @video_thyme_count = medium_consumption.where(sort: "video", + mode: "thyme").count end if @medium.manuscript.present? - @manuscript_access = medium_consumption.where(sort: 'manuscript').pluck(:created_at).map(&:to_date).tally.map { |k, t| + @manuscript_access = medium_consumption + .where(sort: "manuscript") + .pluck(:created_at) + .map(&:to_date).tally.map do |k, t| { x: k, y: t } - }.to_json - @manuscript_access_count = medium_consumption.where(sort: 'manuscript').count - end - if @medium.sort == 'Quiz' - - @quiz_plays = medium_consumption.where(sort: 'quiz', - mode: 'browser').pluck(:created_at).map(&:to_date).tally.map { |k, t| - { x: k, y: t } - }.to_json - @quiz_plays_count = medium_consumption.where(sort: 'quiz', - mode: 'browser').count - @quiz_finished_count = Probe.finished_quizzes(@medium) - @global_success = Probe.global_success_in_quiz(@medium.becomes(Quiz)) - @global_success_details = Probe.global_success_details(@medium.becomes(Quiz)) - @question_count = @medium.becomes(Quiz).questions_count - @local_success = Probe.local_success_in_quiz(@medium.becomes(Quiz)) - end + end.to_json + @manuscript_access_count = medium_consumption.where(sort: "manuscript").count + end + return unless @medium.sort == "Quiz" + + @quiz_plays = medium_consumption + .where(sort: "quiz", mode: "browser") + .pluck(:created_at) + .map(&:to_date).tally.map do |k, t| + { x: k, y: t } + end.to_json + @quiz_plays_count = medium_consumption.where(sort: "quiz", + mode: "browser").count + @quiz_finished_count = Probe.finished_quizzes(@medium) + @global_success = Probe.global_success_in_quiz(@medium.becomes(Quiz)) + @global_success_details = Probe.global_success_details(@medium.becomes(Quiz)) + @question_count = @medium.becomes(Quiz).questions_count + @local_success = Probe.local_success_in_quiz(@medium.becomes(Quiz)) end def show_comments commontator_thread_show(@medium) - render layout: 'application_no_sidebar' + render layout: "application_no_sidebar" end def cancel_publication @@ -457,27 +466,27 @@ def cancel_publication def fill_medium_preview I18n.locale = current_user.locale - @medium = Medium.find_by_id(params[:id])&.becomes(Medium) || Medium.new + @medium = Medium.find_by(id: params[:id])&.becomes(Medium) || Medium.new authorize! :fill_medium_preview, @medium end def render_medium_actions I18n.locale = current_user.locale - @medium = Medium.find_by_id(params[:id])&.becomes(Medium) || Medium.new + @medium = Medium.find_by(id: params[:id])&.becomes(Medium) || Medium.new authorize! :render_medium_actions, @medium end def render_import_media @id = params[:id] - @purpose = 'import' + @purpose = "import" authorize! :render_import_media, Medium.new end def render_import_vertex @id = params[:id] quiz_id = params[:quiz_id] - I18n.locale = Quiz.find_by_id(quiz_id)&.locale_with_inheritance - @purpose = 'quiz' + I18n.locale = Quiz.find_by(id: quiz_id)&.locale_with_inheritance + @purpose = "quiz" authorize! :render_import_vertex, Medium.new render :render_import_media end @@ -492,7 +501,7 @@ def cancel_import_media def cancel_import_vertex authorize! :cancel_import_vertex, Medium.new - I18n.locale = Quiz.find_by_id(params[:quiz_id])&.locale_with_inheritance + I18n.locale = Quiz.find_by(id: params[:quiz_id])&.locale_with_inheritance render :cancel_import_media end @@ -510,9 +519,9 @@ def fill_quizzable_preview def fill_reassign_modal @quizzable = @medium.becomes_quizzable I18n.locale = @quizzable.locale_with_inheritance - @in_quiz = params[:in_quiz] == 'true' + @in_quiz = params[:in_quiz] == "true" @quiz_id = params[:quiz_id].to_i - @no_rights = params[:rights] == 'none' + @no_rights = params[:rights] == "none" end private @@ -539,39 +548,39 @@ def publish_params end def set_medium - @medium = Medium.find_by_id(params[:id])&.becomes(Medium) - return if @medium.present? && @medium.sort != 'RandomQuiz' + @medium = Medium.find_by(id: params[:id])&.becomes(Medium) + return if @medium.present? && @medium.sort != "RandomQuiz" - redirect_to :root, alert: I18n.t('controllers.no_medium') + redirect_to :root, alert: I18n.t("controllers.no_medium") end def set_lecture - @lecture = Lecture.find_by_id(params[:id]) + @lecture = Lecture.find_by(id: params[:id]) # store current lecture in cookie if @lecture cookies[:current_lecture_id] = @lecture.id return end - redirect_to :root, alert: I18n.t('controllers.no_lecture') + redirect_to :root, alert: I18n.t("controllers.no_lecture") end def set_teachable - if params[:teachable_type].in?(['Course', 'Lecture', 'Lesson', 'Talk']) && + if params[:teachable_type].in?(["Course", "Lecture", "Lesson", "Talk"]) && params[:teachable_id].present? @teachable = params[:teachable_type].constantize - .find_by_id(params[:teachable_id]) + .find_by(id: params[:teachable_id]) end end def detach_components - if params[:medium][:detach_video] == 'true' + if params[:medium][:detach_video] == "true" @medium.update(video: nil) @medium.update(screenshot: nil) end - if params[:medium][:detach_geogebra] == 'true' || @medium.sort != 'Sesam' + if params[:medium][:detach_geogebra] == "true" || @medium.sort != "Sesam" @medium.update(geogebra: nil) end - return unless params[:medium][:detach_manuscript] == 'true' + return unless params[:medium][:detach_manuscript] == "true" @medium.update(manuscript: nil) end @@ -580,10 +589,10 @@ def sanitize_params reveal_contradictions sanitize_page! sanitize_per! - params[:all] = (params[:all] == 'true') || (cookies[:all] == 'true') + params[:all] = (params[:all] == "true") || (cookies[:all] == "true") cookies[:all] = params[:all] cookies[:per] = false if cookies[:all] - params[:reverse] = params[:reverse] == 'true' + params[:reverse] = params[:reverse] == "true" end def check_for_consent @@ -613,12 +622,12 @@ def search_results visible_search_results = current_user.filter_visible_media(search_arel) search_results &= visible_search_results total = search_results.size - @lecture = Lecture.find_by_id(params[:id]) + @lecture = Lecture.find_by(id: params[:id]) # filter out stuff from course level for generic users - if params[:visibility] == 'lecture' - search_results.reject! { |m| m.teachable_type == 'Course' } + if params[:visibility] == "lecture" + search_results.reject! { |m| m.teachable_type == "Course" } # yields only lecture media and course media - elsif params[:visibility] == 'all' + elsif params[:visibility] == "all" # yields all lecture media and course media else # this is the default setting: 'thematic' selection of media @@ -627,11 +636,11 @@ def search_results unless current_user.admin || @lecture.edited_by?(current_user) lecture_tags = @lecture.tags_including_media_tags search_results.reject! do |m| - m.teachable_type == 'Course' && (m.tags & lecture_tags).blank? + m.teachable_type == "Course" && !m.tags.intersect?(lecture_tags) end end end - sort = params[:project] == 'keks' ? 'Quiz' : params[:project]&.capitalize + sort = params[:project] == "keks" ? "Quiz" : params[:project]&.capitalize search_results += @lecture.imported_media .where(sort: sort) .locally_visible @@ -643,10 +652,10 @@ def search_results end def reveal_contradictions - return unless params[:lecture_id].present? + return if params[:lecture_id].blank? return if params[:lecture_id].to_i.in?(@course.lecture_ids) - redirect_to :root, alert: I18n.t('controllers.contradiction') + redirect_to :root, alert: I18n.t("controllers.contradiction") end def sanitize_page! @@ -654,9 +663,7 @@ def sanitize_page! end def sanitize_per! - if params[:per] || cookies[:per].to_i.positive? - cookies[:all] = 'false' - end + cookies[:all] = "false" if params[:per] || cookies[:per].to_i.positive? params[:per] = if params[:per].to_i.in?([3, 4, 8, 12, 24, 48]) params[:per].to_i elsif cookies[:per].to_i.positive? @@ -669,8 +676,8 @@ def sanitize_per! def search_params types = params[:search][:types] || [] - types = [types] if types && !types.kind_of?(Array) - types -= [''] if types + types = [types] if types && !types.is_a?(Array) + types -= [""] if types types = nil if types == [] params[:search][:types] = types params[:search][:user_id] = current_user.id @@ -693,29 +700,29 @@ def search_params # destroy all notifications related to this medium def destroy_notifications - Notification.where(notifiable_id: @medium.id, notifiable_type: 'Medium') + Notification.where(notifiable_id: @medium.id, notifiable_type: "Medium") .delete_all end def add_tags_in_lesson_and_sections @tags_outside_lesson = @medium.tags_outside_lesson - if @tags_outside_lesson - @medium.teachable.tags << @tags_outside_lesson - @tags_without_section = @tags_outside_lesson & @medium.teachable.tags_without_section - if @medium.teachable.sections.count == 1 - section = @medium.teachable.sections.first - section.tags << @tags_without_section - end - end + return unless @tags_outside_lesson + + @medium.teachable.tags << @tags_outside_lesson + @tags_without_section = @tags_outside_lesson & @medium.teachable.tags_without_section + return unless @medium.teachable.sections.count == 1 + + section = @medium.teachable.sections.first + section.tags << @tags_without_section end def store_access - mode = action_name == 'play' ? 'thyme' : 'pdf_view' - sort = action_name == 'play' ? 'video' : 'manuscript' + mode = action_name == "play" ? "thyme" : "pdf_view" + sort = action_name == "play" ? "video" : "manuscript" ConsumptionSaver.perform_async(@medium.id, mode, sort) end def store_download - ConsumptionSaver.perform_async(@medium.id, 'download', params[:sort]) + ConsumptionSaver.perform_async(@medium.id, "download", params[:sort]) end end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index a43809799..a6d036f60 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -10,7 +10,7 @@ def current_ability def index @notifications = current_user.notifications.order(:created_at) .reverse_order - render layout: 'application_no_sidebar' + render layout: "application_no_sidebar" end def destroy @@ -27,8 +27,8 @@ def destroy_all # destroy all lecture notifications of current user def destroy_lecture_notifications - lecture = Lecture.find_by_id(params[:lecture_id]) - return unless lecture.present? + lecture = Lecture.find_by(id: params[:lecture_id]) + return if lecture.blank? Notification.delete(current_user.active_notifications(lecture).pluck(:id)) current_user.touch @@ -46,9 +46,9 @@ def destroy_news_notifications private def set_notification - @notification = Notification.find_by_id(params[:id]) + @notification = Notification.find_by(id: params[:id]) return if @notification.present? - redirect_to :root, alert: I18n.t('controllers.no_notification') + redirect_to :root, alert: I18n.t("controllers.no_notification") end end diff --git a/app/controllers/profile_controller.rb b/app/controllers/profile_controller.rb index d74eeb08a..da24afab5 100644 --- a/app/controllers/profile_controller.rb +++ b/app/controllers/profile_controller.rb @@ -16,9 +16,9 @@ def edit return end # destroy the notifications related to new lectures and courses - current_user.notifications.where(notifiable_type: ['Lecture', 'Course']) + current_user.notifications.where(notifiable_type: ["Lecture", "Course"]) .destroy_all - render layout: 'application_no_sidebar' + render layout: "application_no_sidebar" end def update @@ -38,7 +38,7 @@ def update I18n.locale = @locale cookies[:locale] = @locale @user.touch - redirect_to :start, notice: t('profile.success') + redirect_to :start, notice: t("profile.success") else @errors = @user.errors end @@ -54,25 +54,25 @@ def check_for_consent return unless @user.consents redirect_to edit_profile_path, - notice: t('profile.please_update') + notice: t("profile.please_update") end # DSGVO consent action def add_consent - @user.update(consents: true, consented_at: Time.now) - redirect_to :root, notice: t('profile.consent') + @user.update(consents: true, consented_at: Time.zone.now) + redirect_to :root, notice: t("profile.consent") end def toggle_thread_subscription @thread = Commontator::Thread.find(params[:id]) - if @thread && @thread.can_subscribe?(@user) - if params[:subscribe] == 'true' - @thread.subscribe(@user) - else - @thread.unsubscribe(@user) - end - @result = !!@thread.subscription_for(@user) + return unless @thread&.can_subscribe?(@user) + + if params[:subscribe] == "true" + @thread.subscribe(@user) + else + @thread.unsubscribe(@user) end + @result = !!@thread.subscription_for(@user) end def subscribe_lecture @@ -94,18 +94,16 @@ def subscribe_lecture def unsubscribe_lecture @success = current_user.unsubscribe_lecture!(@lecture) @none_left = case @parent - when 'current_subscribed' then current_user.current_subscribed_lectures + when "current_subscribed" then current_user.current_subscribed_lectures .empty? - when 'inactive' then current_user.inactive_lectures.empty? + when "inactive" then current_user.inactive_lectures.empty? end end def star_lecture return unless @lecture&.in?(current_user.lectures) - if !@lecture.in?(current_user.favorite_lectures) - current_user.favorite_lectures << @lecture - end + current_user.favorite_lectures << @lecture unless @lecture.in?(current_user.favorite_lectures) # as favorite lectures appear in the navbar which is cached e.g. in # the lecture show action, make sure the cache is invalidated by # touching the user @@ -122,22 +120,22 @@ def unstar_lecture def show_accordion @collapse_id = params[:id] - redirect_to :root and return unless @collapse_id.present? + redirect_to :root and return if @collapse_id.blank? @lectures = case @collapse_id - when 'collapseCurrentStuff' then current_user.current_subscribed_lectures - when 'collapseInactiveLectures' then current_user.inactive_lectures + when "collapseCurrentStuff" then current_user.current_subscribed_lectures + when "collapseInactiveLectures" then current_user.inactive_lectures .includes(:course, :term) .sort - when 'collapseAllCurrent' then current_user.current_subscribable_lectures + when "collapseAllCurrent" then current_user.current_subscribable_lectures end - @link = @collapse_id.remove('collapse').camelize(:lower) + 'Link' + @link = "#{@collapse_id.remove("collapse").camelize(:lower)}Link" end def request_data MathiMailer.data_request_email(current_user).deliver_later MathiMailer.data_provide_email(current_user).deliver_later - redirect_to edit_profile_path, notice: t('profile.data_request_sent') + redirect_to edit_profile_path, notice: t("profile.data_request_sent") end private @@ -167,10 +165,10 @@ def email_params end def set_lecture - @lecture = Lecture.find_by_id(lecture_params[:id]) + @lecture = Lecture.find_by(id: lecture_params[:id]) @passphrase = lecture_params[:passphrase] @parent = lecture_params[:parent] - @current = !@parent.in?(['lectureSearch', 'inactive']) + @current = !@parent.in?(["lectureSearch", "inactive"]) redirect_to start_path unless @lecture end @@ -180,7 +178,7 @@ def lecture_params # extracts all lecture ids from user params def lecture_ids - params[:user][:lecture].select { |k, v| v == '1' }.keys.map(&:to_i) + params[:user][:lecture].select { |_k, v| v == "1" }.keys.map(&:to_i) end def clean_up_notifications @@ -197,9 +195,9 @@ def clean_up_notifications # if user unsubscribed the lecture the current lecture cookie refers to, # set the lectures cookie to nil def update_lecture_cookie - unless @current_lecture.in?(@user.lectures) - cookies[:current_lecture_id] = nil - end + return if @current_lecture.in?(@user.lectures) + + cookies[:current_lecture_id] = nil end # stop the update if any of passphrases for newly subscribed @@ -215,7 +213,7 @@ def check_passphrases given_passphrase = params[:user][:pass_lecture][l.id.to_s] unless given_passphrase == l.passphrase @errors[:passphrase] ||= [] - @errors[:passphrase].push l.id + @errors[:passphrase].push(l.id) end end end diff --git a/app/controllers/programs_controller.rb b/app/controllers/programs_controller.rb index babb0eb30..28e9aab55 100644 --- a/app/controllers/programs_controller.rb +++ b/app/controllers/programs_controller.rb @@ -7,17 +7,12 @@ def current_ability @current_ability ||= ProgramAbility.new(current_user) end - def edit - end - def new @program = Program.new(subject_id: params[:subject_id].to_i) authorize! :new, @program end - def update - @program.update(program_params) - redirect_to classification_path + def edit end def create @@ -28,6 +23,11 @@ def create redirect_to classification_path end + def update + @program.update(program_params) + redirect_to classification_path + end + def destroy @program.destroy redirect_to classification_path @@ -36,10 +36,10 @@ def destroy private def set_program - @program = Program.find_by_id(params[:id]) + @program = Program.find_by(id: params[:id]) return if @program.present? - redirect_to root_path, alert: I18n.t('controllers.no_program') + redirect_to root_path, alert: I18n.t("controllers.no_program") end def program_params diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 1444e3ba1..71ba8a686 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -4,7 +4,7 @@ class QuestionsController < ApplicationController before_action :set_quizzes, only: [:reassign] before_action :check_solution_errors, only: [:update] authorize_resource except: :reassign - layout 'administration' + layout "administration" def current_ability @current_ability ||= QuestionAbility.new(current_user) @@ -18,7 +18,7 @@ def update return if @errors @success = true if @question.update(question_params) - if question_params[:question_sort] == 'free' + if question_params[:question_sort] == "free" answer = @question.answers.first @question.answers.where.not(id: answer.id).destroy_all end @@ -34,33 +34,34 @@ def update end def reassign - question_old = Question.find_by_id(params[:id]) + question_old = Question.find_by(id: params[:id]) authorize! :reassign, question_old I18n.locale = question_old.locale_with_inheritance @question, answer_map = question_old.duplicate @question.editors = [current_user] @quizzes.each do |q| - Quiz.find_by_id(q).replace_reference!(question_old, @question, answer_map) + Quiz.find_by(id: q).replace_reference!(question_old, @question, answer_map) end I18n.locale = @question.locale_with_inheritance - if question_params[:type] == 'edit' + if question_params[:type] == "edit" redirect_to edit_question_path(@question) return end @quizzable = @question - @mode = 'reassigned' - render 'media/fill_quizzable_area' + @mode = "reassigned" + render "media/fill_quizzable_area" end def set_solution_type - content = if params[:type] == 'MampfExpression' - MampfExpression.trivial_instance - elsif params[:type] == 'MampfMatrix' - MampfMatrix.trivial_instance - elsif params[:type] == 'MampfTuple' - MampfTuple.trivial_instance - elsif params[:type] == 'MampfSet' - MampfSet.trivial_instance + content = case params[:type] + when "MampfExpression" + MampfExpression.trivial_instance + when "MampfMatrix" + MampfMatrix.trivial_instance + when "MampfTuple" + MampfTuple.trivial_instance + when "MampfSet" + MampfSet.trivial_instance end @solution = Solution.new(content) end @@ -78,21 +79,21 @@ def render_question_parameters private def set_question - @question = Question.find_by_id(params[:id]) + @question = Question.find_by(id: params[:id]) return if @question.present? - redirect_to :root, alert: I18n.t('controllers.no_question') + redirect_to :root, alert: I18n.t("controllers.no_question") end def set_quizzes - @quizzes = params[:question].select { |k, v| - v == '1' && k.start_with?('quiz-') - } - .keys.map { |k| k.remove('quiz-').to_i } + quizzes = params[:question].select do |k, v| + v == "1" && k.start_with?("quiz-") + end + @quizzes = quizzes.keys.map { |k| k.remove("quiz-").to_i } end def check_solution_errors - return unless params[:question][:solution_error].present? + return if params[:question][:solution_error].blank? @errors = ActiveModel::Errors.new(@question) @errors.add(:base, params[:question][:solution_error]) diff --git a/app/controllers/quiz_certificates_controller.rb b/app/controllers/quiz_certificates_controller.rb index 0f611e18d..d4f0c9cc6 100644 --- a/app/controllers/quiz_certificates_controller.rb +++ b/app/controllers/quiz_certificates_controller.rb @@ -17,23 +17,23 @@ def claim def validate authorize! :validate, QuizCertificate.new code = certificate_params[:code] - @certificate = QuizCertificate.find_by_code(code) + @certificate = QuizCertificate.find_by(code: code) end private def set_certificate - @certificate = QuizCertificate.find_by_id(params[:id]) + @certificate = QuizCertificate.find_by(id: params[:id]) return if @certificate.present? - redirect_to :root, alert: I18n.t('controllers.no_certificate') + redirect_to :root, alert: I18n.t("controllers.no_certificate") end def check_if_claimed return unless @certificate.user redirect_to :root, - alert: I18n.t('controllers.certificate_already_claimed') + alert: I18n.t("controllers.certificate_already_claimed") end def certificate_params @@ -49,7 +49,7 @@ def set_locale_by_quiz end def set_locale_by_lecture - @lecture = Lecture.find_by_id(certificate_params[:lecture_id]) + @lecture = Lecture.find_by(id: certificate_params[:lecture_id]) I18n.locale = @lecture&.locale_with_inheritance || current_user.locale || I18n.default_locale end diff --git a/app/controllers/quizzes_controller.rb b/app/controllers/quizzes_controller.rb index 3cf4ead9c..b363b18bb 100644 --- a/app/controllers/quizzes_controller.rb +++ b/app/controllers/quizzes_controller.rb @@ -10,7 +10,7 @@ class QuizzesController < ApplicationController before_action :init_values, only: [:take, :proceed] after_action :store_access, only: [:take] authorize_resource except: [:new, :update_branching] - layout 'administration' + layout "administration" def current_ability @current_ability ||= QuizAbility.new(current_user) @@ -38,7 +38,7 @@ def destroy def take I18n.locale = @quiz.locale_with_inheritance - render layout: 'quiz' + render layout: "quiz" end def proceed @@ -81,10 +81,10 @@ def delete_edge end def update_branching - quiz = Quiz.find_by_id(params[:quiz_id]) + quiz = Quiz.find_by(id: params[:quiz_id]) authorize! :update_branching, quiz @quizzable = quiz.quizzable(params[:vertex_id].to_i) - @id = params[:id].sub 'select', 'quizzable' + @id = params[:id].sub("select", "quizzable") end def edit_vertex_targets @@ -99,10 +99,10 @@ def render_vertex_quizzable private def set_quiz - @quiz = Quiz.find_by_id(params[:id]) + @quiz = Quiz.find_by(id: params[:id]) return if @quiz.present? - redirect_to :root, alert: I18n.t('controllers.no_quiz') + redirect_to :root, alert: I18n.t("controllers.no_quiz") end def init_values @@ -115,15 +115,14 @@ def init_values if user_signed_in? && current_user.study_participant quiz_round_params[:study_participant] = current_user.anonymized_id end + quiz_round_params[:save_probe] = - if !user_signed_in? - true - elsif current_user.admin? - false - elsif current_user.in?(Quiz.find(params[:id]).editors_with_inheritance) - false + if user_signed_in? + should_omit_probe = current_user.admin? \ + || current_user.in?(Quiz.find(params[:id]).editors_with_inheritance) + !should_omit_probe else - true + true # always save probe for not signed in users end @quiz_round = QuizRound.new(quiz_round_params) end @@ -133,15 +132,15 @@ def quiz_params end def check_accessibility - return if @quiz.sort == 'RandomQuiz' + return if @quiz.sort == "RandomQuiz" return if user_signed_in? && @quiz.visible_for_user?(current_user) return if !user_signed_in? && @quiz.free? - redirect_to :root, alert: I18n.t('controllers.no_quiz_access') + redirect_to :root, alert: I18n.t("controllers.no_quiz_access") end def check_vertex_accessibility - return if @quiz.sort == 'RandomQuiz' + return if @quiz.sort == "RandomQuiz" if user_signed_in? return if current_user.in?(@quiz.editors_with_inheritance) @@ -150,17 +149,17 @@ def check_vertex_accessibility end return if !user_signed_in? && @quiz.quizzables_free? - redirect_to :root, alert: I18n.t('controllers.no_quiz_vertex_access') + redirect_to :root, alert: I18n.t("controllers.no_quiz_vertex_access") end def check_errors - return if @quiz.sort == 'RandomQuiz' + return if @quiz.sort == "RandomQuiz" return unless @quiz.find_errors&.any? - redirect_to :root, alert: I18n.t('controllers.quiz_has_error') + redirect_to :root, alert: I18n.t("controllers.quiz_has_error") end def store_access - ConsumptionSaver.perform_async(@quiz.id, 'browser', 'quiz') + ConsumptionSaver.perform_async(@quiz.id, "browser", "quiz") end end diff --git a/app/controllers/readers_controller.rb b/app/controllers/readers_controller.rb index 362e5cd86..f276f2989 100644 --- a/app/controllers/readers_controller.rb +++ b/app/controllers/readers_controller.rb @@ -3,7 +3,7 @@ class ReadersController < ApplicationController # no authorization for this controller def update - @thread = Commontator::Thread.find_by_id(reader_params[:thread_id]) + @thread = Commontator::Thread.find_by(id: reader_params[:thread_id]) return unless @thread @reader = Reader.find_or_create_by(user: current_user, @@ -11,7 +11,7 @@ def update @reader.touch @anything_left = current_user.media_latest_comments.any? do |m| (Reader.find_by(user: current_user, thread: m[:thread]) - &.updated_at || (Time.now - 1000.years)) < m[:latest_comment].created_at + &.updated_at || 1000.years.ago) < m[:latest_comment].created_at end current_user.update(unread_comments: false) unless @anything_left end @@ -26,8 +26,7 @@ def update_all new_readers << Reader.new(thread_id: t, user: current_user) end Reader.import new_readers - Reader.where(user: current_user, thread: threads) - .update_all(updated_at: Time.now) + Reader.where(user: current_user, thread: threads).touch_all current_user.update(unread_comments: false) end diff --git a/app/controllers/referrals_controller.rb b/app/controllers/referrals_controller.rb index 4d07fa221..3d06fed3e 100644 --- a/app/controllers/referrals_controller.rb +++ b/app/controllers/referrals_controller.rb @@ -8,35 +8,23 @@ def current_ability @current_ability ||= ReferralAbility.new(current_user) end - def update - I18n.locale = @referral.medium.locale_with_inheritance - # if referral's item is a link, it is updated - # this means in particular that *all referrals* that refer to it will - # be affected; links are changed *globally* - update_item if Item.find_by_id(@item_id)&.sort == 'link' - return if @errors.present? - - @referral.update(updated_params) - @errors = @referral.errors unless @referral.valid? - end - def edit I18n.locale = @referral.medium.locale_with_inheritance # if referral's item is a link, load all other links, # otherwise load all items in the referral's item's medium scope # that the user can choose from in the item dropdown menu - @item_selection = if @referral.item.sort == 'link' + @item_selection = if @referral.item.sort == "link" Item.where(medium: nil) .map { |i| [i.description, i.id] } else @referral.item.medium.teachable.media_scope .media_items_with_inheritance end - @item = Item.new(sort: 'link') + @item = Item.new(sort: "link") end def create - update_item if Item.find_by_id(@item_id)&.sort == 'link' + update_item if Item.find_by(id: @item_id)&.sort == "link" if @errors.present? render :update return @@ -48,6 +36,18 @@ def create render :update end + def update + I18n.locale = @referral.medium.locale_with_inheritance + # if referral's item is a link, it is updated + # this means in particular that *all referrals* that refer to it will + # be affected; links are changed *globally* + update_item if Item.find_by(id: @item_id)&.sort == "link" + return if @errors.present? + + @referral.update(updated_params) + @errors = @referral.errors unless @referral.valid? + end + def destroy @medium = @referral.medium @referral.destroy @@ -58,11 +58,11 @@ def destroy # renders it in json as it will be called by ajax def list_items authorize! :list_items, Referral.new - teachable_id = params[:teachable_id].to_s.split('-') - if teachable_id[0] == 'external' + teachable_id = params[:teachable_id].to_s.split("-") + if teachable_id[0] == "external" result = Item.where(medium: nil).pluck(:description, :id) else - @teachable = teachable_id[0].constantize.find_by_id(teachable_id[1]) + @teachable = teachable_id[0].constantize.find_by(id: teachable_id[1]) result = @teachable.media_items_with_inheritance end result ||= Item.none diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 0cd93ae14..bc9152b78 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,20 +1,20 @@ -require 'net/http' -require 'uri' -require 'json' +require "net/http" +require "uri" +require "json" # RegistrationsController class RegistrationsController < Devise::RegistrationsController prepend_before_action :check_registration_limit, only: [:create] def verify_captcha - return true unless ENV['USE_CAPTCHA_SERVICE'] + return true unless ENV["USE_CAPTCHA_SERVICE"] begin - uri = URI.parse(ENV['CAPTCHA_VERIFY_URL']) + uri = URI.parse(ENV.fetch("CAPTCHA_VERIFY_URL", nil)) data = { message: params["frc-captcha-solution"], - application_token: ENV['CAPTCHA_APPLICATION_TOKEN'] } - header = { 'Content-Type': 'text/json' } + application_token: ENV.fetch("CAPTCHA_APPLICATION_TOKEN", nil) } + header = { "Content-Type": "text/json" } http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true if ENV['CAPTCHA_VERIFY_URL'].include?('https') + http.use_ssl = true if ENV["CAPTCHA_VERIFY_URL"].include?("https") request = Net::HTTP::Post.new(uri.request_uri, header) request.body = data.to_json @@ -22,9 +22,9 @@ def verify_captcha response = http.request(request) answer = JSON.parse(response.body) return true if answer["message"] == "verified" - rescue + rescue StandardError # rubocop:todo Lint/SuppressedException end - return false + false end def create @@ -33,50 +33,54 @@ def create else build_resource(devise_parameter_sanitizer.sanitize(:sign_up)) clean_up_passwords(resource) - set_flash_message :alert, :captcha_error + set_flash_message(:alert, :captcha_error) render :new end end def destroy password_correct = resource.valid_password?(deletion_params[:password]) - if !password_correct - set_flash_message :alert, :password_incorrect - respond_with_navigational(resource) { + unless password_correct + set_flash_message(:alert, :password_incorrect) + respond_with_navigational(resource) do redirect_to after_sign_up_path_for(resource_name) - } + end return end success = resource.archive_and_destroy(deletion_params[:archive_name]) - if !success - set_flash_message :alert, :not_destroyed - respond_with_navigational(resource) { + unless success + set_flash_message(:alert, :not_destroyed) + respond_with_navigational(resource) do redirect_to after_sign_up_path_for(resource_name) - } + end return end Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name) - set_flash_message :notice, :destroyed - yield resource if block_given? - respond_with_navigational(resource) { + set_flash_message(:notice, :destroyed) + yield(resource) if block_given? + respond_with_navigational(resource) do redirect_to after_sign_out_path_for(resource_name) - } + end end - def after_sign_up_path_for(resource) + def after_sign_up_path_for(_resource) edit_profile_path end private def check_registration_limit - if User.where("users.confirmed_at is NULL and users.created_at > '#{(DateTime.now() - (ENV['MAMPF_REGISTRATION_TIMEFRAME'] || 15).to_i.minutes)}'").count > (ENV['MAMPF_MAX_REGISTRATION_PER_TIMEFRAME'] || 40).to_i - self.resource = resource_class.new devise_parameter_sanitizer.sanitize(:sign_up) - resource.validate # Look for any other validation errors besides reCAPTCHA - set_flash_message :alert, :too_many_registrations - set_minimum_password_length - respond_with_navigational(resource) { render :new } - end + timeframe = ((ENV["MAMPF_REGISTRATION_TIMEFRAME"] || 15).to_i.minutes.ago..) + num_new_registrations = User.where(confirmed_at: nil, created_at: timeframe).count + max_registrations = (ENV["MAMPF_MAX_REGISTRATION_PER_TIMEFRAME"] || 40).to_i + return if num_new_registrations <= max_registrations + + # Current number of new registrations is too high + self.resource = resource_class.new(devise_parameter_sanitizer.sanitize(:sign_up)) + resource.validate # Look for any other validation errors besides reCAPTCHA + set_flash_message(:alert, :too_many_registrations) + set_minimum_password_length + respond_with_navigational(resource) { render :new } end def deletion_params diff --git a/app/controllers/remarks_controller.rb b/app/controllers/remarks_controller.rb index 9d95e9779..c1ecd275a 100644 --- a/app/controllers/remarks_controller.rb +++ b/app/controllers/remarks_controller.rb @@ -3,7 +3,7 @@ class RemarksController < MediaController before_action :set_remark, except: :reassign before_action :set_quizzes, only: [:reassign] authorize_resource except: :reassign - layout 'administration' + layout "administration" def current_ability @current_ability ||= RemarkAbility.new(current_user) @@ -18,22 +18,22 @@ def update end def reassign - remark_old = Remark.find_by_id(params[:id]) + remark_old = Remark.find_by(id: params[:id]) authorize! :reassign, remark_old I18n.locale = remark_old.locale_with_inheritance @remark = remark_old.duplicate @remark.editors = [current_user] @quizzes.each do |q| - Quiz.find_by_id(q).replace_reference!(remark_old, @remark) + Quiz.find_by(id: q).replace_reference!(remark_old, @remark) end I18n.locale = @remark.locale_with_inheritance - if remark_params[:type] == 'edit' + if remark_params[:type] == "edit" redirect_to edit_remark_path(@remark) return end @quizzable = @remark - @mode = 'reassigned' - render 'media/fill_quizzable_area' + @mode = "reassigned" + render "media/fill_quizzable_area" end def cancel_remark_basics @@ -42,15 +42,15 @@ def cancel_remark_basics private def set_remark - @remark = Remark.find_by_id(params[:id]) + @remark = Remark.find_by(id: params[:id]) return if @remark.present? - redirect_to remarks_path, alert: I18n.t('controllers.no_remark') + redirect_to remarks_path, alert: I18n.t("controllers.no_remark") end def set_quizzes - @quizzes = params[:remark].select { |_k, v| v == '1' }.keys - .map { |k| k.remove('quiz-').to_i } + @quizzes = params[:remark].select { |_k, v| v == "1" }.keys + .map { |k| k.remove("quiz-").to_i } end def remark_params diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index ef9d49636..67b432ea2 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -12,7 +12,7 @@ def current_ability def index search_down = @search_string.downcase # determine tags whose title contains the search string - matches = Notion.all.pluck(:tag_id, :title, :aliased_tag_id) + matches = Notion.pluck(:tag_id, :title, :aliased_tag_id) .select { |x| x.second.downcase.include?(search_down) } .map { |a| a.first || a.third }.uniq @tags = Tag.where(id: matches) @@ -40,13 +40,13 @@ def set_search_string def sanitize_search_string if @search_string.nil? redirect_back fallback_location: root_path, - alert: I18n.t('controllers.no_search_term') + alert: I18n.t("controllers.no_search_term") return end return if @search_string.length > 1 redirect_back fallback_location: root_path, - alert: I18n.t('controllers.search_term_short') + alert: I18n.t("controllers.search_term_short") end def find_similar_tags diff --git a/app/controllers/sections_controller.rb b/app/controllers/sections_controller.rb index f7a87b997..6fa9cae43 100644 --- a/app/controllers/sections_controller.rb +++ b/app/controllers/sections_controller.rb @@ -2,7 +2,7 @@ class SectionsController < ApplicationController before_action :set_section, except: [:new, :create] authorize_resource except: [:new, :create] - layout 'administration' + layout "administration" def current_ability @current_ability ||= SectionAbility.new(current_user) @@ -10,20 +10,20 @@ def current_ability def show I18n.locale = @section.lecture.locale_with_inheritance - render layout: 'application_no_sidebar' - end - - def edit - I18n.locale = @section.lecture.locale_with_inheritance + render layout: "application_no_sidebar" end def new - @chapter = Chapter.find_by_id(params[:chapter_id]) + @chapter = Chapter.find_by(id: params[:chapter_id]) @section = Section.new(chapter: @chapter) authorize! :new, @section I18n.locale = @section.lecture.locale_with_inheritance end + def edit + I18n.locale = @section.lecture.locale_with_inheritance + end + def create @section = Section.new(section_params) authorize! :create, @section @@ -31,12 +31,6 @@ def create @errors = @section.errors end - def destroy - @lecture = @section.lecture - @section.destroy - redirect_to edit_lecture_path(@lecture) - end - def update I18n.locale = @section.lecture.locale_with_inheritance @old_chapter = @section.chapter @@ -50,6 +44,12 @@ def update @errors = @section.errors end + def destroy + @lecture = @section.lecture + @section.destroy + redirect_to edit_lecture_path(@lecture) + end + def display I18n.locale = @section.lecture.locale_with_inheritance end @@ -57,10 +57,10 @@ def display private def set_section - @section = Section.find_by_id(params[:id]) + @section = Section.find_by(id: params[:id]) return if @section.present? - redirect_to :root, alert: I18n.t('controllers.no_section') + redirect_to :root, alert: I18n.t("controllers.no_section") end def section_params @@ -83,12 +83,10 @@ def insert_or_save # updates the position of the section if predecessor is given def update_position predecessor = params[:section][:predecessor] - return unless predecessor.present? + return if predecessor.blank? position = predecessor.to_i - if position > @section.position && @old_chapter == @section.chapter - position -= 1 - end + position -= 1 if position > @section.position && @old_chapter == @section.chapter @section.insert_at(position + 1) end diff --git a/app/controllers/subjects_controller.rb b/app/controllers/subjects_controller.rb index 6cd036d20..29fa26cf4 100644 --- a/app/controllers/subjects_controller.rb +++ b/app/controllers/subjects_controller.rb @@ -15,11 +15,6 @@ def new def edit end - def update - @subject.update(subject_params) - redirect_to classification_path - end - def create @subject = Subject.new(subject_params) authorize! :create, @subject @@ -27,6 +22,11 @@ def create redirect_to classification_path end + def update + @subject.update(subject_params) + redirect_to classification_path + end + def destroy @subject.destroy redirect_to classification_path @@ -35,10 +35,10 @@ def destroy private def set_subject - @subject = Subject.find_by_id(params[:id]) + @subject = Subject.find_by(id: params[:id]) return if @subject.present? - redirect_to root_path, alert: I18n.t('controllers.no_answer') + redirect_to root_path, alert: I18n.t("controllers.no_answer") end def subject_params diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index ad4422df9..870bf22eb 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -16,13 +16,13 @@ def current_ability @current_ability ||= SubmissionAbility.new(current_user) end - # note: authorization for #index is done manually via before_actions + # NOTE: authorization for #index is done manually via before_actions # SubmissionAbility lets anyone pass def index @assignments = @lecture.assignments @current_assignments = @lecture.current_assignments @previous_assignments = @lecture.previous_assignments - @old_assignments = @assignments.expired.order('deadline DESC') - + @old_assignments = @assignments.expired.order("deadline DESC") - @previous_assignments @future_assignments = @assignments.active.order(:deadline) - @current_assignments @@ -37,6 +37,33 @@ def new def edit end + def create + @submission = Submission.new(submission_create_params) + @lecture = @submission&.assignment&.lecture + set_submission_locale + @too_late = @submission.not_updatable? + return if @too_late + + if submission_manuscript_params[:manuscript].present? + @submission.manuscript = submission_manuscript_params[:manuscript] + @errors = @submission.check_file_properties(@submission.manuscript + .metadata, + :manuscript) + return if @errors.present? + end + @submission.user_submission_joins.build(user: current_user) + @submission.save + @assignment = @submission.assignment + @errors = @submission.errors + return unless @submission.valid? + + send_invitation_emails + @submission.update(last_modification_by_users_at: Time.zone.now) + return unless @submission.manuscript + + send_upload_email(User.where(id: current_user.id)) + end + def update return if @too_late @@ -56,45 +83,18 @@ def update @submission.update(submission_update_params) if @submission.valid? @submission.update(accepted: nil) - if params[:submission][:detach_user_manuscript] == 'true' + if params[:submission][:detach_user_manuscript] == "true" @submission.update(manuscript: nil, - last_modification_by_users_at: Time.now) + last_modification_by_users_at: Time.zone.now) send_upload_removal_email(@submission.users) elsif @submission.manuscript_data != old_manuscript_data - @submission.update(last_modification_by_users_at: Time.now) + @submission.update(last_modification_by_users_at: Time.zone.now) send_upload_email(@submission.users) end end @errors = @submission.errors end - def create - @submission = Submission.new(submission_create_params) - @lecture = @submission&.assignment&.lecture - set_submission_locale - @too_late = @submission.not_updatable? - return if @too_late - - if submission_manuscript_params[:manuscript].present? - @submission.manuscript = submission_manuscript_params[:manuscript] - @errors = @submission.check_file_properties(@submission.manuscript - .metadata, - :manuscript) - return if @errors.present? - end - @submission.user_submission_joins.build(user: current_user) - @submission.save - @assignment = @submission.assignment - @errors = @submission.errors - return unless @submission.valid? - - send_invitation_emails - @submission.update(last_modification_by_users_at: Time.now) - return unless @submission.manuscript - - send_upload_email(User.where(id: current_user.id)) - end - def destroy return if @too_late @@ -111,16 +111,16 @@ def redeem_code check_code_and_join unless @error redirect_to lecture_submissions_path(@submission.tutorial.lecture), - notice: t('submission.joined_successfully', + notice: t("submission.joined_successfully", assignment: @submission.assignment.title) return end - redirect_to :start, alert: t('submission.failed_redemption', + redirect_to :start, alert: t("submission.failed_redemption", message: @error) end def join - @assignment = Assignment.find_by_id(join_params[:assignment_id]) + @assignment = Assignment.find_by(id: join_params[:assignment_id]) @lecture = @assignment.lecture set_submission_locale code = join_params[:code] @@ -132,7 +132,7 @@ def leave return if @too_late if @submission.users.count == 1 - @error = I18n.t('submission.no_partners_no_leave') + @error = I18n.t("submission.no_partners_no_leave") return end @submission.users.delete(current_user) @@ -146,28 +146,28 @@ def cancel_new end def show_manuscript - if @submission && @submission.manuscript - send_file @submission.manuscript.to_io, + if @submission&.manuscript + send_file(@submission.manuscript.to_io, type: @submission.manuscript_mime_type, disposition: @disposition, - filename: @submission.manuscript_filename + filename: @submission.manuscript_filename) elsif @submission - redirect_to :start, alert: t('submission.no_manuscript_yet') + redirect_to :start, alert: t("submission.no_manuscript_yet") else - redirect_to :start, alert: t('submission.exists_no_longer') + redirect_to :start, alert: t("submission.exists_no_longer") end end def show_correction - if @submission && @submission.correction - send_file @submission.correction.to_io, + if @submission&.correction + send_file(@submission.correction.to_io, type: @submission.correction_mime_type, disposition: @disposition, - filename: @submission.correction_filename + filename: @submission.correction_filename) elsif @submission - redirect_to :start, alert: t('submission.no_correction_yet') + redirect_to :start, alert: t("submission.no_correction_yet") else - redirect_to :start, alert: t('submission.exists_no_longer') + redirect_to :start, alert: t("submission.exists_no_longer") end end @@ -245,13 +245,13 @@ def reject private def set_submission - @submission = Submission.find_by_id(params[:id]) + @submission = Submission.find_by(id: params[:id]) @assignment = @submission&.assignment @lecture = @assignment&.lecture set_submission_locale return if @submission - flash[:alert] = I18n.t('controllers.no_submission') + flash.now[:alert] = I18n.t("controllers.no_submission") render js: "window.location='#{root_path}'" end @@ -270,21 +270,21 @@ def submission_manuscript_params end def set_assignment - @assignment = Assignment.find_by_id(params[:assignment_id]) + @assignment = Assignment.find_by(id: params[:assignment_id]) @lecture = @assignment&.lecture set_submission_locale return if @assignment - flash[:alert] = I18n.t('controllers.no_assignment') + flash.now[:alert] = I18n.t("controllers.no_assignment") render js: "window.location='#{root_path}'" - return + nil end def set_lecture - @lecture = Lecture.find_by_id(params[:id]) + @lecture = Lecture.find_by(id: params[:id]) set_submission_locale and return if @lecture - redirect_to :root, alert: I18n.t('controllers.no_lecture') + redirect_to :root, alert: I18n.t("controllers.no_lecture") end def set_too_late @@ -378,34 +378,34 @@ def send_rejection_email(users) def check_code_validity if !@submission && @assignment - @error = I18n.t('submission.invalid_code_for_assignment', + @error = I18n.t("submission.invalid_code_for_assignment", assignment: @assignment.title) elsif !@submission - @error = I18n.t('submission.invalid_code') + @error = I18n.t("submission.invalid_code") elsif @assignment&.totally_expired? - @error = I18n.t('submission.assignment_expired') + @error = I18n.t("submission.assignment_expired") elsif @submission.correction - @error = I18n.t('submission.already_corrected') + @error = I18n.t("submission.already_corrected") elsif current_user.in?(@submission.users) - @error = I18n.t('submission.already_in') + @error = I18n.t("submission.already_in") elsif !@submission.tutorial.lecture.in?(current_user.lectures) - @error = I18n.t('submission.lecture_not_subscribed') + @error = I18n.t("submission.lecture_not_subscribed") end end def check_code_and_join check_code_validity - unless @error - @join = UserSubmissionJoin.new(user: current_user, - submission: @submission) - @join.save - if @join.valid? - @submission.update(last_modification_by_users_at: Time.now) - send_join_email - remove_invitee_status - else - @error = @join.errors[:base].join(', ') - end + return if @error + + @join = UserSubmissionJoin.new(user: current_user, + submission: @submission) + @join.save + if @join.valid? + @submission.update(last_modification_by_users_at: Time.zone.now) + send_join_email + remove_invitee_status + else + @error = @join.errors[:base].join(", ") end end @@ -438,26 +438,26 @@ def check_student_status return if current_user.proper_student_in?(@lecture) redirect_to :root, - alert: I18n.t('controllers.no_student_status_in_lecture') + alert: I18n.t("controllers.no_student_status_in_lecture") end def check_if_tutorials return if @lecture.tutorials.any? - redirect_to :root, alert: I18n.t('controllers.no_tutorials_in_lecture') + redirect_to :root, alert: I18n.t("controllers.no_tutorials_in_lecture") end def check_if_assignments return if @lecture.assignments.any? - redirect_to :root, alert: I18n.t('controllers.no_assignments_in_lecture') + redirect_to :root, alert: I18n.t("controllers.no_assignments_in_lecture") end def set_disposition - @disposition = params[:download] == 'true' ? 'attachment' : 'inline' + @disposition = params[:download] == "true" ? "attachment" : "inline" accepted = @submission.assignment.accepted_file_type return unless accepted.in?(Assignment.non_inline_file_types) - @disposition = 'attachment' + @disposition = "attachment" end end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 574a42d84..8b5c49d28 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -9,38 +9,43 @@ class TagsController < ApplicationController before_action :check_creation_permission, only: [:create] authorize_resource except: [:new, :modal, :search, :postprocess, :render_tag_title] - layout 'administration' + layout "administration" def current_ability @current_ability ||= TagAbility.new(current_user) end def show - if params[:locale].in?(I18n.available_locales.map(&:to_s)) - I18n.locale = params[:locale] - end + I18n.locale = params[:locale] if params[:locale].in?(I18n.available_locales.map(&:to_s)) set_related_tags_for_user @lectures = current_user.filter_lectures(@tag.lectures) # first, filter the media according to the users subscription type media = current_user.filter_media(@tag.media - .where.not(sort: ['Question', - 'Remark'])) + .where.not(sort: ["Question", + "Remark"])) # then, filter these according to their visibility for the user @media = current_user.filter_visible_media(media) @questions = @tag.visible_questions(current_user) # consider items in manuscripts that are corresponding to tags - manuscripts = current_user.filter_media(Medium.where(sort: 'Script')) + manuscripts = current_user.filter_media(Medium.where(sort: "Script")) @references = Item.where(medium: manuscripts, description: @tag.notions.pluck(:title) + @tag.aliases.pluck(:title)) - .where.not(pdf_destination: [nil, '']) + .where.not(pdf_destination: [nil, ""]) @realizations = @tag.realizations - render layout: 'application_no_sidebar' + render layout: "application_no_sidebar" end def display_cyto set_related_tags_for_user - render layout: 'cytoscape' + render layout: "cytoscape" + end + + def new + @tag = Tag.new + authorize! :new, @tag + set_notions + @tag.aliases.new(locale: I18n.locale) end def edit @@ -51,11 +56,20 @@ def edit @tag.aliases.new(locale: I18n.locale) end - def new - @tag = Tag.new - authorize! :new, @tag - set_notions - @tag.aliases.new(locale: I18n.locale) + def create + # first, check if errors from creation_permission callback are present + @section = Section.find_by(id: params[:tag][:section_id]) + if @errors.present? + render :update + return + end + @tag.update(tag_params) + if @tag.valid? && !@modal + redirect_to edit_tag_path(@tag) + return + end + @errors = @tag.errors + render :update end def update @@ -74,22 +88,6 @@ def update @errors = @tag.errors end - def create - # first, check if errors from creation_permission callback are present - @section = Section.find_by_id(params[:tag][:section_id]) - if @errors.present? - render :update - return - end - @tag.update(tag_params) - if @tag.valid? && !@modal - redirect_to edit_tag_path(@tag) - return - end - @errors = @tag.errors - render :update - end - def destroy @tag.destroy redirect_to administration_path @@ -110,7 +108,7 @@ def modal end def identify - @identified_tag = Tag.find_by_id(params[:tag][:identified_tag_id]) + @identified_tag = Tag.find_by(id: params[:tag][:identified_tag_id]) @tag.identify_with!(@identified_tag) @identified_tag.destroy @tag.update(tag_params) @@ -118,9 +116,7 @@ def identify end def fill_tag_select - if params[:locale].in?(I18n.available_locales.map(&:to_s)) - I18n.locale = params[:locale] - end + I18n.locale = params[:locale] if params[:locale].in?(I18n.available_locales.map(&:to_s)) if params[:q] result = Tag.select_with_substring(params[:q]) render json: result @@ -131,7 +127,7 @@ def fill_tag_select end def fill_course_tags - course = Course.find_by_id(params[:course_id]) + course = Course.find_by(id: params[:course_id]) result = course&.select_question_tags_by_title render json: result end @@ -141,16 +137,16 @@ def search per_page = search_params[:per] || 10 search = Sunspot.new_search(Tag) search.build do - fulltext search_params[:title] + fulltext(search_params[:title]) end - course_ids = if search_params[:all_courses] == '1' + course_ids = if search_params[:all_courses] == "1" [] - elsif search_params[:course_ids] != [''] + elsif search_params[:course_ids] != [""] search_params[:course_ids] end search.build do with(:course_ids, course_ids) - paginate page: params[:page], per_page: per_page + paginate(page: params[:page], per_page: per_page) end search.execute results = search.results @@ -168,41 +164,39 @@ def postprocess authorize! :postprocess, Tag.new @tags_hash = params[:tags] @tags_hash.each do |t, section_data| - tag = Tag.find_by_id(t) + tag = Tag.find_by(id: t) next unless tag section_data.each do |s, v| - next if v.to_i == 0 + next if v.to_i.zero? section = Section.find(s) next unless section - if !tag.in?(section.tags) - section.tags << tag - end + section.tags << tag unless tag.in?(section.tags) end end - if params['from'] == 'Lesson' - redirect_to edit_lesson_path(Lesson.find_by_id(params[:id])) + if params["from"] == "Lesson" + redirect_to edit_lesson_path(Lesson.find_by(id: params[:id])) return end - redirect_to edit_medium_path(Medium.find_by_id(params[:id])) + redirect_to edit_medium_path(Medium.find_by(id: params[:id])) end def render_tag_title authorize! :render_tag_title, Tag.new - tag = Tag.find_by_id(params[:tag_id]) - @identified_tag = Tag.find_by_id(params[:identified_tag_id]) + tag = Tag.find_by(id: params[:tag_id]) + @identified_tag = Tag.find_by(id: params[:identified_tag_id]) @common_titles = tag.common_titles(@identified_tag) end private def set_tag - @tag = Tag.find_by_id(params[:id]) + @tag = Tag.find_by(id: params[:id]) return if @tag.present? - redirect_to :root, alert: I18n.t('controllers.no_tag') + redirect_to :root, alert: I18n.t("controllers.no_tag") end # set up cytoscape graph data for neighbourhood subgraph of @tag, @@ -215,9 +209,7 @@ def set_related_tags_for_user @depth = depth_param if depth_param.in?([1, 2]) overrule_subscription_type = false selection = params[:selection].to_i - if selection.in?([1, 2, 3]) - overrule_subscription_type = selection - end + overrule_subscription_type = selection if selection.in?([1, 2, 3]) @selection_type = if overrule_subscription_type selection else @@ -246,45 +238,45 @@ def set_related_tags def set_up_tag @tag = Tag.new set_notions - related_tag = Tag.find_by_id(params[:related_tag]) + related_tag = Tag.find_by(id: params[:related_tag]) @tag.related_tags << related_tag if related_tag.present? end def add_course - course = Course.find_by_id(params[:course]) + course = Course.find_by(id: params[:course]) @tag.courses << course if course.present? end def add_section - section = Section.find_by_id(params[:section]) - if section - @tag.sections << section - I18n.locale = section.lecture.locale || current_user.locale - end + section = Section.find_by(id: params[:section]) + return unless section + + @tag.sections << section + I18n.locale = section.lecture.locale || current_user.locale end def add_medium - medium = Medium.find_by_id(params[:medium]) - if medium - I18n.locale = medium.locale_with_inheritance || current_user.locale - @tag.media << medium - end + medium = Medium.find_by(id: params[:medium]) + return unless medium + + I18n.locale = medium.locale_with_inheritance || current_user.locale + @tag.media << medium end def add_lesson - lesson = Lesson.find_by_id(params[:lesson]) - if lesson - @tag.lessons << lesson - I18n.locale = lesson.lecture.locale || current_user.locale - end + lesson = Lesson.find_by(id: params[:lesson]) + return unless lesson + + @tag.lessons << lesson + I18n.locale = lesson.lecture.locale || current_user.locale end def add_talk - talk = Talk.find_by_id(params[:talk]) - if talk - @tag.talks << talk - I18n.locale = talk.lecture.locale || current_user.locale - end + talk = Talk.find_by(id: params[:talk]) + return unless talk + + @tag.talks << talk + I18n.locale = talk.lecture.locale || current_user.locale end def check_for_consent @@ -305,8 +297,8 @@ def tag_params end def realization_params - (params.require(:tag).permit(realizations: [])[:realizations] - ['']) - .map { |r| r.split('-') } + (params.require(:tag).permit(realizations: [])[:realizations] - [""]) + .map { |r| r.split("-") } .map { |x| [x.first, x.second.to_i] } end @@ -322,16 +314,16 @@ def check_permissions def permission_errors errors = [] unless removed_courses.all? { |c| c.removable_by?(current_user) } - errors.push(error_hash['remove_course']) + errors.push(error_hash["remove_course"]) end unless added_courses.all? { |c| c.addable_by?(current_user) } - errors.push(error_hash['add_course']) + errors.push(error_hash["add_course"]) end @errors[:courses] = errors if errors.present? end def check_creation_permission - @modal = (params[:tag][:modal] == 'true') + @modal = (params[:tag][:modal] == "true") @tag = Tag.new check_permissions end @@ -352,19 +344,20 @@ def set_notions end def locale - locale = if params[:from] == 'course' - @tag.courses&.first&.locale - elsif params[:from] == 'medium' - @tag.media&.first&.locale_with_inheritance - elsif params[:from] == 'section' - @tag.sections&.first&.lecture&.locale_with_inheritance + locale = case params[:from] + when "course" + @tag.courses&.first&.locale + when "medium" + @tag.media&.first&.locale_with_inheritance + when "section" + @tag.sections&.first&.lecture&.locale_with_inheritance end locale || current_user.locale end def error_hash - { 'remove_course' => I18n.t('controllers.no_removal_rights'), - 'add_course' => I18n.t('controllers.no_adding_rights') } + { "remove_course" => I18n.t("controllers.no_removal_rights"), + "add_course" => I18n.t("controllers.no_adding_rights") } end def search_params diff --git a/app/controllers/talks_controller.rb b/app/controllers/talks_controller.rb index 475fed70f..01a9a8a5c 100644 --- a/app/controllers/talks_controller.rb +++ b/app/controllers/talks_controller.rb @@ -3,14 +3,18 @@ class TalksController < ApplicationController before_action :set_talk, except: [:new, :create] authorize_resource except: [:new, :create] before_action :set_view_locale, only: [:edit] - layout 'administration' + layout "administration" def current_ability @current_ability ||= TalkAbility.new(current_user) end + def show + render layout: "application_no_sidebar" + end + def new - @lecture = Lecture.find_by_id(params[:lecture_id]) + @lecture = Lecture.find_by(id: params[:lecture_id]) @talk = Talk.new(lecture: @lecture) authorize! :new, @talk I18n.locale = @talk.lecture.locale_with_inheritance || @@ -20,14 +24,10 @@ def new def edit end - def show - render layout: 'application_no_sidebar' - end - def create @talk = Talk.new(talk_params) authorize! :create, @talk - dates = params[:talk][:dates].values.compact - [''] + dates = params[:talk][:dates].values.compact - [""] @talk.dates = dates if dates I18n.locale = @talk&.lecture&.locale_with_inheritance || current_user.locale || I18n.default_locale @@ -45,7 +45,7 @@ def create def update I18n.locale = @talk.lecture.locale_with_inheritance || current_user.locale || I18n.default_locale - dates = params[:talk][:dates]&.values&.compact.to_a - [''] + dates = params[:talk][:dates]&.values&.compact.to_a - [""] @talk.update(talk_params) @talk.update(dates: dates) if dates && @talk.valid? if @talk.valid? @@ -69,7 +69,7 @@ def destroy end def assemble - render layout: 'application_no_sidebar' + render layout: "application_no_sidebar" end # modify is the update action for speakers of the talk @@ -82,15 +82,15 @@ def modify private def set_talk - @talk = Talk.find_by_id(params[:id]) + @talk = Talk.find_by(id: params[:id]) return if @talk.present? - redirect_to :root, alert: I18n.t('controllers.no_talk') + redirect_to :root, alert: I18n.t("controllers.no_talk") end def talk_params attributes = [:title, :lecture_id, :details, :description, - :display_description, speaker_ids: [], tag_ids: []] + :display_description, { speaker_ids: [], tag_ids: [] }] if @talk && !current_user.in?(@talk.speakers) && !@talk.display_description attributes.delete(:display_description) diff --git a/app/controllers/terms_controller.rb b/app/controllers/terms_controller.rb index af473afe0..9727bff09 100644 --- a/app/controllers/terms_controller.rb +++ b/app/controllers/terms_controller.rb @@ -1,9 +1,9 @@ # TermsController class TermsController < ApplicationController before_action :set_term, except: [:index, :new, :create, :cancel, :set_active] - layout 'administration' + layout "administration" authorize_resource except: [:index, :new, :create, :cancel, :set_active] - layout 'administration' + layout "administration" def current_ability @current_ability ||= TermAbility.new(current_user) @@ -11,12 +11,7 @@ def current_ability def index authorize! :index, Term.new - @terms = Term.order(:year, :season).reverse_order.page params[:page] - end - - def destroy - @term.destroy - redirect_to terms_path + @terms = Term.order(:year, :season).reverse_order.page(params[:page]) end def new @@ -24,6 +19,9 @@ def new authorize! :new, @term end + def edit + end + def create @term = Term.new(term_params) authorize! :create, @term @@ -32,28 +30,30 @@ def create redirect_to terms_path return end - @errors = @term.errors[:season].join(', ') + @errors = @term.errors[:season].join(", ") render :update end - def edit - end - def update @term.update(term_params) - @errors = @term.errors[:season].join(', ') unless @term.valid? + @errors = @term.errors[:season].join(", ") unless @term.valid? + end + + def destroy + @term.destroy + redirect_to terms_path end def cancel @id = params[:id] - @term = Term.find_by_id(@id) + @term = Term.find_by(id: @id) authorize! :cancel, @term - @new_action = params[:new] == 'true' + @new_action = params[:new] == "true" end def set_active authorize! :set_active, Term.new - new_active_term = Term.find_by_id(active_term_params[:active_term]) + new_active_term = Term.find_by(id: active_term_params[:active_term]) old_active_term = Term.active if old_active_term && new_active_term && new_active_term != old_active_term old_active_term.update(active: false) @@ -68,10 +68,10 @@ def set_active def set_term @id = params[:id] - @term = Term.find_by_id(@id) + @term = Term.find_by(id: @id) return if @term - redirect_to terms_path, alert: I18n.t('controllers.no_term') + redirect_to terms_path, alert: I18n.t("controllers.no_term") end def term_params diff --git a/app/controllers/tutorials_controller.rb b/app/controllers/tutorials_controller.rb index f849a708a..21287ed79 100644 --- a/app/controllers/tutorials_controller.rb +++ b/app/controllers/tutorials_controller.rb @@ -6,7 +6,7 @@ class TutorialsController < ApplicationController :bulk_upload, :export_teams] before_action :set_assignment, only: [:bulk_download_submissions, - :bulk_download_corrections´, + :bulk_download_corrections, :bulk_upload, :export_teams] before_action :set_lecture, only: [:index, :overview] @@ -15,8 +15,8 @@ class TutorialsController < ApplicationController authorize_resource except: [:index, :overview, :create, :validate_certificate, :new, :cancel_new] - require 'rubygems' - require 'zip' + require "rubygems" + require "zip" def current_ability @current_ability ||= TutorialAbility.new(current_user) @@ -24,35 +24,38 @@ def current_ability def index authorize! :index, Tutorial.new, @lecture - @assignments = @lecture.assignments.order('deadline DESC') - @assignment = Assignment.find_by_id(params[:assignment]) || + @assignments = @lecture.assignments.order("deadline DESC") + @assignment = Assignment.find_by(id: params[:assignment]) || @assignments&.first - if current_user.editor_or_teacher_in?(@lecture) - @tutorials = @lecture.tutorials + @tutorials = if current_user.editor_or_teacher_in?(@lecture) + @lecture.tutorials else - @tutorials = current_user.given_tutorials.where(lecture: @lecture) + current_user.given_tutorials.where(lecture: @lecture) end - @tutorial = Tutorial.find_by_id(params[:tutorial]) || current_user.tutorials(@lecture).first + @tutorial = Tutorial.find_by(id: params[:tutorial]) || current_user.tutorials(@lecture).first @stack = @assignment&.submissions&.where(tutorial: @tutorial)&.proper &.order(:last_modification_by_users_at) end def overview authorize! :overview, Tutorial.new, @lecture - @assignments = @lecture.assignments.order('deadline DESC') - @assignment = Assignment.find_by_id(params[:assignment]) || + @assignments = @lecture.assignments.order("deadline DESC") + @assignment = Assignment.find_by(id: params[:assignment]) || @assignments&.first @tutorials = @lecture.tutorials end def new @tutorial = Tutorial.new - @lecture = Lecture.find_by_id(params[:lecture_id]) + @lecture = Lecture.find_by(id: params[:lecture_id]) set_tutorial_locale @tutorial.lecture = @lecture authorize! :new, @tutorial end + def edit + end + def create @tutorial = Tutorial.new(tutorial_params) authorize! :create, @tutorial @@ -62,13 +65,10 @@ def create @errors = @tutorial.errors end - def edit - end - def update @tutorial.update(tutorial_params) @errors = @tutorial.errors - return if @errors.present? + nil if @errors.present? end def destroy @@ -79,7 +79,7 @@ def cancel_edit end def cancel_new - @lecture = Lecture.find_by_id(params[:lecture]) + @lecture = Lecture.find_by(id: params[:lecture]) authorize! :cancel_new, Tutorial.new(lecture: @lecture) set_tutorial_locale @none_left = @lecture&.tutorials&.none? @@ -92,67 +92,65 @@ def bulk_download_submissions def bulk_download_corrections @zipped_corrections = Submission.zip_corrections!(@tutorial, @assignment) - bulk_download(@zipped_corrections, '-Corrections') + bulk_download(@zipped_corrections, "-Corrections") end def bulk_upload - begin - files = JSON.parse(params[:files]) - @report = Submission.bulk_corrections!(@tutorial, @assignment, files) - @stack = @assignment.submissions.where(tutorial: @tutorial).proper - .order(:last_modification_by_users_at) - send_correction_upload_emails - # in case an empty string for files is sent - rescue JSON::ParserError - flash[:alert] = I18n.t('tutorial.bulk_upload.error') - end + files = JSON.parse(params[:files]) + @report = Submission.bulk_corrections!(@tutorial, @assignment, files) + @stack = @assignment.submissions.where(tutorial: @tutorial).proper + .order(:last_modification_by_users_at) + send_correction_upload_emails + # in case an empty string for files is sent + rescue JSON::ParserError + flash[:alert] = I18n.t("tutorial.bulk_upload.error") end def validate_certificate authorize! :validate_certificate, Tutorial.new - @lecture = Lecture.find_by_id(params[:lecture_id]) + @lecture = Lecture.find_by(id: params[:lecture_id]) set_tutorial_locale end def export_teams respond_to do |format| format.html { head :ok } - format.csv { - send_data @tutorial.teams_to_csv(@assignment), - filename: "#{@tutorial.title}-#{@assignment.title}.csv" - } + format.csv do + send_data(@tutorial.teams_to_csv(@assignment), + filename: "#{@tutorial.title}-#{@assignment.title}.csv") + end end end private def set_tutorial - @tutorial = Tutorial.find_by_id(params[:id]) + @tutorial = Tutorial.find_by(id: params[:id]) @lecture = @tutorial&.lecture set_tutorial_locale and return if @tutorial - redirect_to :root, alert: I18n.t('controllers.no_tutorial') + redirect_to :root, alert: I18n.t("controllers.no_tutorial") end def set_assignment - @assignment = Assignment.find_by_id(params[:ass_id]) + @assignment = Assignment.find_by(id: params[:ass_id]) return if @assignment - redirect_to :root, alert: I18n.t('controllers.no_assignment') + redirect_to :root, alert: I18n.t("controllers.no_assignment") end def set_lecture - @lecture = Lecture.find_by_id(params[:id]) + @lecture = Lecture.find_by(id: params[:id]) set_tutorial_locale and return if @lecture - redirect_to :root, alert: I18n.t('controllers.no_lecture') + redirect_to :root, alert: I18n.t("controllers.no_lecture") end def set_lecture_from_form - @lecture = Lecture.find_by_id(tutorial_params[:lecture_id]) + @lecture = Lecture.find_by(id: tutorial_params[:lecture_id]) return if @lecture - redirect_to :root, alert: I18n.t('controllers.no_lecture') + redirect_to :root, alert: I18n.t("controllers.no_lecture") end def set_tutorial_locale @@ -163,7 +161,7 @@ def set_tutorial_locale def can_view_index return if current_user.in?(@lecture.tutors) || current_user.editor_or_teacher_in?(@lecture) - redirect_to :root, alert: I18n.t('controllers.no_tutor_in_this_lecture') + redirect_to :root, alert: I18n.t("controllers.no_tutor_in_this_lecture") end def tutorial_params @@ -174,14 +172,14 @@ def bulk_params params.permit(:package) end - def bulk_download(zipped, end_of_file = '') + def bulk_download(zipped, end_of_file = "") if zipped.is_a?(StringIO) - send_data zipped.read, - filename: @assignment.title + '@' + @tutorial.title + end_of_file + '.zip', - type: 'application/zip', - disposition: 'attachment' + send_data(zipped.read, + filename: "#{@assignment.title}@#{@tutorial.title}#{end_of_file}.zip", + type: "application/zip", + disposition: "attachment") else - flash[:alert] = I18n.t('controllers.tutorials.bulk_download_failed', + flash[:alert] = I18n.t("controllers.tutorials.bulk_download_failed", message: zipped) redirect_to lecture_tutorials_path(@tutorial.lecture, params: diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 0f03a0aca..750f5631c 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,7 +3,7 @@ class UsersController < ApplicationController before_action :set_elevated_users, only: [:index, :list_generic_users] before_action :set_user, only: [:edit, :update, :destroy] - layout 'administration' + layout "administration" def current_ability @current_ability ||= UserAbility.new(current_user) @@ -24,7 +24,7 @@ def update old_image_data = @user.image_data @user.update(user_params) @errors = @user.errors - @user.update(image: nil) if params[:user][:detach_image] == 'true' + @user.update(image: nil) if params[:user][:detach_image] == "true" changed_image = @user.image_data != old_image_data if @user.image.present? && changed_image @user.image_derivatives! @@ -38,12 +38,12 @@ def elevate authorize! :elevate, User.new @errors = {} @user = User.find(elevate_params[:id]) - admin = elevate_params[:admin] == '1' + admin = elevate_params[:admin] == "1" return unless admin # enforce a name if @user.name.blank? - name = @user.email.split('@')[0] + name = @user.email.split("@")[0] @user.update(admin: true, name: name) else @user.update(admin: true) @@ -64,14 +64,14 @@ def destroy end def teacher - @teacher = User.find_by_id(params[:teacher_id]) + @teacher = User.find_by(id: params[:teacher_id]) authorize! :teacher, @teacher if @teacher.present? && @teacher.teacher? - render layout: 'application' + render layout: "application" return end redirect_to :root, - alert: I18n.t('controllers.no_teacher') + alert: I18n.t("controllers.no_teacher") end def fill_user_select @@ -103,10 +103,10 @@ def user_params end def set_user - @user = User.find_by_id(params[:id]) + @user = User.find_by(id: params[:id]) return unless @user.nil? - redirect_to :root, alert: I18n.t('controllers.no_medium') + redirect_to :root, alert: I18n.t("controllers.no_medium") end def set_elevated_users diff --git a/app/controllers/vertices_controller.rb b/app/controllers/vertices_controller.rb index 3f3e027d9..102a12823 100644 --- a/app/controllers/vertices_controller.rb +++ b/app/controllers/vertices_controller.rb @@ -1,7 +1,7 @@ # VerticesController class VerticesController < ApplicationController before_action :set_values - # note that we do not use cancancan's authorization methods in the actions + # NOTE: that we do not use cancancan's authorization methods in the actions # as we could not get it to work here # it seems not to accept the quiz parameter: # authorize! :new, :vertex, @quiz @@ -52,7 +52,7 @@ def destroy def set_values @quiz_id = params[:quiz_id] - @quiz = Quiz.find_by_id(@quiz_id) + @quiz = Quiz.find_by(id: @quiz_id) @params_v = params[:vertex] end @@ -65,9 +65,9 @@ def set_update_vertex_params def set_create_vertex_params @sort = @params_v[:sort] - if @sort == 'import' + if @sort == "import" @quizzables = Medium.where(id: @params_v[:quizzable_ids], - type: ['Question', 'Remark']) + type: ["Question", "Remark"]) @success = @quizzables.any? else quizzable = @sort.constantize.create_prefilled(@params_v[:label], @@ -80,24 +80,24 @@ def set_create_vertex_params def set_branching_hash @branching = {} - @params_v.keys.select { |k| k.start_with?('branching-') }.each do |k| - next if @params_v[k].to_i == 0 + @params_v.keys.select { |k| k.start_with?("branching-") }.each do |k| + next if @params_v[k].to_i.zero? - @branching[k.remove('branching-').to_h] = + @branching[k.remove("branching-").to_h] = [@vertex_id, @params_v[k].to_i] end end def set_hide_array - @hide = @params_v.keys.select { |k| k.start_with?('hide-') } - .select { |h| @params_v[h] == '1' } - .map { |h| h.remove('hide-').to_h } + @hide = @params_v.keys.select { |k| k.start_with?("hide-") } + .select { |h| @params_v[h] == "1" } + .map { |h| h.remove("hide-").to_h } end def check_permission return if current_user.admin return if current_user.can_edit?(@quiz) - redirect_to :root, alert: I18n.t('controllers.unauthorized') + redirect_to :root, alert: I18n.t("controllers.unauthorized") end end diff --git a/app/controllers/watchlist_entries_controller.rb b/app/controllers/watchlist_entries_controller.rb index 106e8644e..cf1e3f288 100644 --- a/app/controllers/watchlist_entries_controller.rb +++ b/app/controllers/watchlist_entries_controller.rb @@ -6,15 +6,13 @@ def current_ability def create @watchlist_entry = WatchlistEntry.new - @watchlist = Watchlist.find_by_id(params[:watchlist_entry][:watchlist_id]) + @watchlist = Watchlist.find_by(id: params[:watchlist_entry][:watchlist_id]) @watchlist_entry.watchlist = @watchlist - @medium = Medium.find_by_id(params[:watchlist_entry][:medium_id]) + @medium = Medium.find_by(id: params[:watchlist_entry][:medium_id]) @watchlist_entry.medium = @medium authorize! :create, @watchlist_entry @success = @watchlist_entry.save - if @success - flash[:notice] = I18n.t('watchlist_entry.add_success') - end + flash[:notice] = I18n.t("watchlist_entry.add_success") if @success respond_to do |format| format.js end @@ -24,9 +22,9 @@ def destroy @watchlist_entry = WatchlistEntry.find(params[:id]) authorize! :destroy, @watchlist_entry @watchlist_entry.destroy - flash[:notice] = I18n.t('watchlist_entry.deletion') - redirect_to controller: 'watchlists', - action: 'show', + flash[:notice] = I18n.t("watchlist_entry.deletion") + redirect_to controller: "watchlists", + action: "show", id: params[:watchlist], all: params[:all], reverse: params[:reverse], diff --git a/app/controllers/watchlists_controller.rb b/app/controllers/watchlists_controller.rb index fc616dbf9..7e4cbf49c 100644 --- a/app/controllers/watchlists_controller.rb +++ b/app/controllers/watchlists_controller.rb @@ -5,26 +5,47 @@ class WatchlistsController < ApplicationController before_action :sanitize_params, only: [:show, :update_order, :change_visibility] - layout 'application_no_sidebar' + layout "application_no_sidebar" def current_ability @current_ability ||= WatchlistAbility.new(current_user) end + def index + authorize! :index, Watchlist + @watchlists = current_user.watchlists + if @watchlists.present? + redirect_to watchlist_path(@watchlists.first) + return + end + render "show" + end + + def show + authorize! :show, @watchlist + @watchlists = current_user.watchlists + return if @watchlist.watchlist_entries.empty? + + @watchlist_entries = paginated_results + @media = @watchlist_entries.pluck(:medium_id) + end + def new authorize! :new, Watchlist end + def edit + authorize! :edit, @watchlist + end + def create @watchlist = Watchlist.new(name: create_params[:name], user: current_user, description: create_params[:description]) authorize! :create, @watchlist - @medium = Medium.find_by_id(create_params[:medium_id]) + @medium = Medium.find_by(id: create_params[:medium_id]) @success = @watchlist.save - if @medium.blank? && @success - flash[:notice] = I18n.t('watchlist.creation_success') - end + flash[:notice] = I18n.t("watchlist.creation_success") if @medium.blank? && @success respond_to do |format| format.js end @@ -33,63 +54,38 @@ def create def update authorize! :update, @watchlist @success = @watchlist.update(update_params) - if @success - flash[:notice] = I18n.t('watchlist.change_success') - end + flash[:notice] = I18n.t("watchlist.change_success") if @success respond_to do |format| format.js end end - def edit - authorize! :edit, @watchlist - end - def destroy authorize! :destroy, @watchlist @success = @watchlist.destroy if @success - flash[:notice] = I18n.t('watchlist.delete_success') + flash[:notice] = I18n.t("watchlist.delete_success") else - flash[:alert] = I18n.t('watchlist.delete_failed') + flash[:alert] = I18n.t("watchlist.delete_failed") end redirect_to watchlists_path end - def index - authorize! :index, Watchlist - @watchlists = current_user.watchlists - if @watchlists.present? - redirect_to watchlist_path(@watchlists.first) - return - end - render 'show' - end - - def show - authorize! :show, @watchlist - @watchlists = current_user.watchlists - return if @watchlist.watchlist_entries.empty? - - @watchlist_entries = paginated_results - @media = @watchlist_entries.pluck(:medium_id) - end - def add_medium authorize! :add_medium, Watchlist @watchlists = current_user.watchlists - @medium = Medium.find_by_id(params[:medium_id]) + @medium = Medium.find_by(id: params[:medium_id]) end def update_order - entries = params[:order].map { |id| WatchlistEntry.find_by_id(id) } + entries = params[:order].map { |id| WatchlistEntry.find_by(id: id) } authorize! :update_order, @watchlist, entries page = params[:page].to_i per = params[:per].to_i if params[:reverse] entries.reverse! - shift = @watchlist.watchlist_entries.size - page * per unless page == 0 + shift = @watchlist.watchlist_entries.size - (page * per) unless page.zero? else shift = page * per end @@ -106,15 +102,15 @@ def change_visibility private def set_watchlist - @watchlist = Watchlist.find_by_id(params[:id]) + @watchlist = Watchlist.find_by(id: params[:id]) return if @watchlist.present? - redirect_to :root, alert: I18n.t('controllers.no_watchlist') + redirect_to :root, alert: I18n.t("controllers.no_watchlist") end def sanitize_params - params[:reverse] = params[:reverse] == 'true' - params[:public] = params[:public] == 'true' + params[:reverse] = params[:reverse] == "true" + params[:public] = params[:public] == "true" end def paginated_results diff --git a/app/helpers/announcements_helper.rb b/app/helpers/announcements_helper.rb index 9994c564b..13d4cc709 100644 --- a/app/helpers/announcements_helper.rb +++ b/app/helpers/announcements_helper.rb @@ -3,26 +3,24 @@ module AnnouncementsHelper # create text for notification about new announcement in notification dropdown # menu def announcement_notification_item_header(announcement) - unless announcement.lecture.present? - return t('notifications.mampf_announcement') - end + return t("notifications.mampf_announcement") if announcement.lecture.blank? - t('notifications.lecture_announcement', + t("notifications.lecture_announcement", title: announcement.lecture.title_for_viewers) end # make announcements cards colored if the announcement is active def news_card_color(announcement) - return '' unless user_signed_in? - return 'bg-post-it-blue' if announcement.active?(current_user) + return "" unless user_signed_in? + return "bg-post-it-blue" if announcement.active?(current_user) - '' + "" end # create text for lecture announcement in notification card header def announcement_notification_card_header(announcement) link_to(announcement.lecture.title_for_viewers, announcement.lecture.path(current_user), - class: 'text-dark') + class: "text-dark") end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 24a7684a9..50f6b333b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -14,7 +14,13 @@ def current_lecture # Returns the complete url for the media upload folder if in production def host - Rails.env.production? ? ENV['MEDIA_SERVER'] + '/' + ENV['INSTANCE_NAME'] : '' + if Rails.env.production? + # rubocop:disable Style/StringConcatenation + ENV.fetch("MEDIA_SERVER", nil) + "/" + ENV.fetch("INSTANCE_NAME", nil) + # rubocop:enable Style/StringConcatenation + else + "" + end end # The HTML download attribute only works for files within the domain of @@ -23,15 +29,15 @@ def host # the actual media server. # This is used for the download buttons for videos and manuscripts. def download_host - Rails.env.production? ? ENV['DOWNLOAD_LOCATION'] : '' + Rails.env.production? ? ENV.fetch("DOWNLOAD_LOCATION", nil) : "" end # Returns the full title on a per-page basis. - def full_title(page_title = '') - return page_title if action_name == 'play' && controller_name == 'media' - return 'Quiz' if action_name == 'take' && controller_name == 'quizzes' + def full_title(page_title = "") + return page_title if action_name == "play" && controller_name == "media" + return "Quiz" if action_name == "take" && controller_name == "quizzes" - base_title = 'MaMpf' + base_title = "MaMpf" if user_signed_in? && current_user.notifications.any? base_title += " (#{current_user.notifications.size})" end @@ -40,92 +46,92 @@ def full_title(page_title = '') # next methods are service methods for the display status of HTML elmements def hide(value) - value ? 'none;' : 'block;' + value ? "none;" : "block;" end def show(value) - value ? 'block;' : 'none;' + value ? "block;" : "none;" end def show_inline(value) - value ? 'inline;' : 'none;' + value ? "inline;" : "none;" end def show_no_block(value) - value ? '' : 'none;' + value ? "" : "none;" end # active attribute for navs def active(value) - value ? 'active' : '' + value ? "active" : "" end # show/collapse attributes for collapses and accordions def show_collapse(value) - value ? 'show collapse' : 'collapse' + value ? "show collapse" : "collapse" end def show_tab(value) - value ? 'show active' : '' + value ? "show active" : "" end def text_dark(value) - value ? '' : 'text-dark' + value ? "" : "text-dark" end def text_dark_link(value) - value ? 'text-primary' : 'text-dark' + value ? "text-primary" : "text-dark" end # media_sort -> database fields def media_types - { 'kaviar' => ['Kaviar'], 'sesam' => ['Sesam'], - 'keks' => ['Quiz'], - 'kiwi' => ['Kiwi'], - 'erdbeere' => ['Erdbeere'], 'nuesse' => ['Nuesse'], - 'script' => ['Script'], 'questions' => ['Question'], - 'remarks' => ['Remark'], 'reste' => ['Reste'] } + { "kaviar" => ["Kaviar"], "sesam" => ["Sesam"], + "keks" => ["Quiz"], + "kiwi" => ["Kiwi"], + "erdbeere" => ["Erdbeere"], "nuesse" => ["Nuesse"], + "script" => ["Script"], "questions" => ["Question"], + "remarks" => ["Remark"], "reste" => ["Reste"] } end # media_sorts def media_sorts - %w[kaviar sesam keks kiwi erdbeere nuesse script - questions remarks reste] + ["kaviar", "sesam", "keks", "kiwi", "erdbeere", "nuesse", "script", "questions", "remarks", + "reste"] end # media_sort -> acronym def media_names - { 'kaviar' => t('categories.kaviar.plural'), - 'sesam' => t('categories.sesam.plural'), - 'keks' => t('categories.quiz.plural'), - 'kiwi' => t('categories.kiwi.singular'), - 'erdbeere' => t('categories.erdbeere.singular'), - 'nuesse' => t('categories.exercises.plural'), - 'script' => t('categories.script.singular'), - 'questions' => t('categories.question.plural'), - 'remarks' => t('categories.remark.plural'), - 'reste' => t('categories.reste.singular') } + { "kaviar" => t("categories.kaviar.plural"), + "sesam" => t("categories.sesam.plural"), + "keks" => t("categories.quiz.plural"), + "kiwi" => t("categories.kiwi.singular"), + "erdbeere" => t("categories.erdbeere.singular"), + "nuesse" => t("categories.exercises.plural"), + "script" => t("categories.script.singular"), + "questions" => t("categories.question.plural"), + "remarks" => t("categories.remark.plural"), + "reste" => t("categories.reste.singular") } end # Selects all media associated to lectures and lessons from a given list # of media def lecture_media(media) - media.where(teachable_type: %w[Lecture Lesson]) + media.where(teachable_type: ["Lecture", "Lesson"]) end # Selects all media associated to courses from a given list of media def course_media(media) - media.where(teachable_type: 'Course') + media.where(teachable_type: "Course") end # For a given list of media, returns the array of courses and lectures # the given media are associated to. def lecture_course_teachables(media) teachables = media.pluck(:teachable_type, :teachable_id).uniq - course_ids = teachables.select { |t| t.first == 'Course' }.map(&:second) - lecture_ids = teachables.select { |t| t.first == 'Lecture' }.map(&:second) - lesson_ids = teachables.select { |t| t.first == 'Lesson' }.map(&:second) - talk_ids = teachables.select { |t| t.first == 'Talk' }.map(&:second) + course_ids = teachables.select { |t| t.first == "Course" }.map(&:second) + lecture_ids = teachables.select { |t| t.first == "Lecture" }.map(&:second) + lesson_ids = teachables.select { |t| t.first == "Lesson" }.map(&:second) + talk_ids = teachables.select { |t| t.first == "Talk" }.map(&:second) lecture_ids += Lesson.where(id: lesson_ids).pluck(:lecture_id).uniq lecture_ids += Talk.where(id: talk_ids).pluck(:lecture_id).uniq Course.where(id: course_ids) + Lecture.where(id: lecture_ids.uniq) @@ -137,7 +143,6 @@ def lecture_course_teachables(media) # (b) associated to the given lecture or a lesson associated to the given # lecture def relevant_media(teachable, media, limit) - result = [] if teachable.instance_of?(Course) return media.where(teachable: teachable).order(:created_at) .reverse_order @@ -149,7 +154,7 @@ def relevant_media(teachable, media, limit) # splits an array into smaller parts def split_list(list, pieces = 4) - group_size = (list.count / pieces) != 0 ? list.count / pieces : 1 + group_size = (list.count / pieces).zero? ? 1 : list.count / pieces groups = list.in_groups_of(group_size) diff = groups.count - pieces return groups if diff <= 0 @@ -161,7 +166,7 @@ def split_list(list, pieces = 4) # returns true for 'media#enrich' action def enrich?(controller, action) - return true if controller == 'media' && action == 'enrich' + return true if controller == "media" && action == "enrich" false end @@ -169,38 +174,38 @@ def enrich?(controller, action) # cuts off a given string so that a given number of letters is not exceeded # string is given ... as ending if it is too long def shorten(title, max_letters) - return '' if title.blank? + return "" if title.blank? return title unless title.length > max_letters - title[0, max_letters - 3] + '...' + "#{title[0, max_letters - 3]}..." end # Returns the grouped list of all courses/lectures/references together # with their ids. Is used in grouped_options_for_select in form helpers. def grouped_teachable_list list = [] - Course.all.each do |c| - lectures = [[c.short_title + ' (' + t('basics.all') + ')', - 'Course-' + c.id.to_s]] - c.lectures.includes(:term).each do |l| - lectures.push [l.short_title_release, 'Lecture-' + l.id.to_s] + Course.find_each do |c| + lectures = [["#{c.short_title} (#{t("basics.all")})", + "Course-#{c.id}"]] + c.lectures.includes(:term).find_each do |l| + lectures.push([l.short_title_release, "Lecture-#{l.id}"]) end - list.push [c.title, lectures] + list.push([c.title, lectures]) end - list.push [t('admin.referral.external_references'), - [[t('admin.referral.external_all'), 'external-0']]] + list.push([t("admin.referral.external_references"), + [[t("admin.referral.external_all"), "external-0"]]]) end # Returns the grouped list of all courses/lectures together with their ids. # Is used in grouped_options_for_select in form helpers def grouped_teachable_list_alternative list = [] - Course.all.each do |c| - lectures = [[c.short_title + ' Modul', 'Course-' + c.id.to_s]] - c.lectures.includes(:term).each do |l| - lectures.push [l.short_title, 'Lecture-' + l.id.to_s] + Course.find_each do |c| + lectures = [["#{c.short_title} Modul", "Course-#{c.id}"]] + c.lectures.includes(:term).find_each do |l| + lectures.push([l.short_title, "Lecture-#{l.id}"]) end - list.push [c.title, lectures] + list.push([c.title, lectures]) end list end @@ -231,57 +236,53 @@ def edit_or_inspect_medium_path(medium) # anything older than today or yesterday gets reduced to the day.month.year # yesterday's/today's dates are return as 'gestern/heute' plus hour:mins def human_readable_date(date) - if date.to_date == Date.today - return t('today') + ', ' + date.strftime('%H:%M') - end - if date.to_date == Date.yesterday - return t('yesterday') + ', ' + date.strftime('%H:%M') - end + return "#{t("today")}, #{date.strftime("%H:%M")}" if date.to_date == Time.zone.today + return "#{t("yesterday")}, #{date.strftime("%H:%M")}" if date.to_date == Date.yesterday - I18n.localize date, format: :concise + I18n.l(date, format: :concise) end # prepend a select prompt to selection for options_for_select def add_prompt(selection) - [[t('basics.select'), '']] + selection + [[t("basics.select"), ""]] + selection end def quizzable_color(type) - 'bg-' + type.downcase + "bg-#{type.downcase}" end def questioncolor(value) - value ? 'bg-question' : '' + value ? "bg-question" : "" end def vertex_label(quiz, vertex_id) - vertex_id.to_s + ' ' + quiz.quizzable(vertex_id)&.label.to_s + "#{vertex_id} #{quiz.quizzable(vertex_id)&.label}" end def ballot_box(correctness) - raw(correctness ? '☒' : '☐') + raw(correctness ? "☒" : "☐") # rubocop:disable Rails/OutputSafety end def boxcolor(correctness) - correctness ? 'correct' : 'incorrect' + correctness ? "correct" : "incorrect" end def bgcolor(correctness) - correctness ? 'bg-correct' : 'bg-incorrect' + correctness ? "bg-correct" : "bg-incorrect" end def hide_as_class(value) - value ? 'no_display' : '' + value ? "no_display" : "" end - def helpdesk(text, html, title = t('info')) - tag.i class: 'far fa-question-circle helpdesk ms-2', + def helpdesk(text, html, title = t("info")) + tag.i(class: "far fa-question-circle helpdesk ms-2", tabindex: -1, - 'data-bs-toggle': 'popover', - 'data-bs-trigger': 'focus', - 'data-bs-content': text, - 'data-bs-html': html, - title: title + "data-bs-toggle": "popover", + "data-bs-trigger": "focus", + "data-bs-content": text, + "data-bs-html": html, + title: title) end def realization_path(realization) @@ -291,10 +292,10 @@ def realization_path(realization) def first_course_independent? current_user.administrated_courses .natural_sort_by(&:title) - &.first&.term_independent + &.first&.term_independent end - def get_announcements + def main_page_announcements megaphone_icon_str = '' separator_str = "
#{megaphone_icon_str}" Announcement.active_on_main @@ -305,29 +306,25 @@ def get_announcements # Navbar items styling based on which page we are on # https://gist.github.com/mynameispj/5692162 - $active_css_class = 'active-item' + ACTIVE_CSS_CLASS = "active-item".freeze def get_class_for_project(project) - request.params['project'] == project ? $active_css_class : '' + request.params["project"] == project ? ACTIVE_CSS_CLASS : "" end def get_class_for_path(path) - request.path == path ? $active_css_class : '' + request.path == path ? ACTIVE_CSS_CLASS : "" end def get_class_for_path_startswith(path) - request.path.starts_with?(path) ? $active_css_class : '' + request.path.starts_with?(path) ? ACTIVE_CSS_CLASS : "" end def get_class_for_any_path(paths) - paths.include?(request.path) ? $active_css_class : '' + paths.include?(request.path) ? ACTIVE_CSS_CLASS : "" end def get_class_for_any_path_startswith(paths) - if paths.any? { |path| request.path.starts_with?(path) } - return $active_css_class - end - - '' + paths.any? { |path| request.path.starts_with?(path) } ? ACTIVE_CSS_CLASS : "" end end diff --git a/app/helpers/assignments_helper.rb b/app/helpers/assignments_helper.rb index ce0369e2f..6f6a01ad5 100644 --- a/app/helpers/assignments_helper.rb +++ b/app/helpers/assignments_helper.rb @@ -6,18 +6,9 @@ def cancel_editing_assignment_path(assignment) cancel_new_assignment_path(params: { lecture: assignment.lecture }) end - def has_documents?(assignment) - return false unless assignment.medium - - assignment.medium.video || assignment.medium.manuscript || - assignment.medium.geogebra || - assignment.medium.external_reference_link.present? || - (assignment.medium.sort == 'Quiz' && assignment.medium.quiz_graph) - end - def file_button_text(assignment) - return I18n.t('basics.file') unless assignment.accepted_file_type == '.pdf' + return I18n.t("basics.file") unless assignment.accepted_file_type == ".pdf" - I18n.t('basics.files') + I18n.t("basics.files") end end diff --git a/app/helpers/chapters_helper.rb b/app/helpers/chapters_helper.rb index 8d806f67b..bf4b454a1 100644 --- a/app/helpers/chapters_helper.rb +++ b/app/helpers/chapters_helper.rb @@ -1,7 +1,7 @@ # Chapters Helper module ChaptersHelper def chapter_positions_for_select(chapter) - [[t('basics.at_the_beginning'), 0]] + chapter.lecture.select_chapters - + [[t("basics.at_the_beginning"), 0]] + chapter.lecture.select_chapters - [[chapter.to_label, chapter.position]] end end diff --git a/app/helpers/clickers_helper.rb b/app/helpers/clickers_helper.rb index cde787a1e..d2477f2de 100644 --- a/app/helpers/clickers_helper.rb +++ b/app/helpers/clickers_helper.rb @@ -1,10 +1,10 @@ # Clickers Helper module ClickersHelper def generate_qr(text) - require 'barby' - require 'barby/barcode' - require 'barby/barcode/qr_code' - require 'barby/outputter/png_outputter' + require "barby" + require "barby/barcode" + require "barby/barcode/qr_code" + require "barby/outputter/png_outputter" qrcode = Barby::QrCode.new(text, level: :h, size: 8) base64_output = Base64.encode64(qrcode.to_png({ xdim: 8 })) diff --git a/app/helpers/courses_helper.rb b/app/helpers/courses_helper.rb index d205eb5e0..749726aec 100644 --- a/app/helpers/courses_helper.rb +++ b/app/helpers/courses_helper.rb @@ -2,43 +2,41 @@ module CoursesHelper # create text for notification about new course in notification dropdown menu def course_notification_item_header(course) - t('notifications.new_course', title: course.title) + t("notifications.new_course", title: course.title) end # create text for notification card - def course_notification_item_details(course) - t('notifications.subscribe_course') + def course_notification_item_details(_course) + t("notifications.subscribe_course") end # create text for notification about new course in notification card def course_notification_card_text(course) - t('notifications.new_course_created_html', title: course.title) + t("notifications.new_course_created_html", title: course.title) end # create link for notification about new lecture in notification card def course_notification_card_link - t('notifications.subscribe_course_html', - profile: link_to(t('notifications.profile'), + t("notifications.subscribe_course_html", + profile: link_to(t("notifications.profile"), edit_profile_path, - class: 'darkblue')) + class: "darkblue")) end def course_link_or_text(course, user) - unless user.admin || user.in?(course.editors) - return course.title - end + return course.title unless user.admin || user.in?(course.editors) link_to(course.title, edit_course_path(course)) end def course_edit_icon(course) - link_to edit_course_path(course), - class: 'text-dark me-2', - style: 'text-decoration: none;', - data: { toggle: 'tooltip', - placement: 'bottom' }, - title: t('buttons.edit') do - tag.i class: 'far fa-edit' + link_to(edit_course_path(course), + class: "text-dark me-2", + style: "text-decoration: none;", + data: { toggle: "tooltip", + placement: "bottom" }, + title: t("buttons.edit")) do + tag.i(class: "far fa-edit") end end end diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb index 17ea7a4e3..1031b37fc 100644 --- a/app/helpers/email_helper.rb +++ b/app/helpers/email_helper.rb @@ -1,6 +1,6 @@ module EmailHelper def email_image_tag(image, **options) - attachments.inline[image] = File.read(Rails.root.join("public/#{image}")) - image_tag attachments[image].url, **options + attachments.inline[image] = Rails.root.join("public/#{image}").read + image_tag(attachments[image].url, **options) end end diff --git a/app/helpers/items_helper.rb b/app/helpers/items_helper.rb index 217a5c6cc..a3de12bee 100644 --- a/app/helpers/items_helper.rb +++ b/app/helpers/items_helper.rb @@ -3,7 +3,7 @@ module ItemsHelper # returns the list of sections for the given item, # as is used in options_for_select def select_sections(item) - [[I18n.t('admin.item.no_section'), '']] + + [[I18n.t("admin.item.no_section"), ""]] + item.medium.teachable&.section_selection end @@ -16,12 +16,12 @@ def select_script_items(lecture) end def check_unless_hidden(item_id) - return 'checked' unless Item.find_by_id(item_id)&.hidden + return "checked" unless Item.find_by(id: item_id)&.hidden - '' + "" end def check_status(content) - content['hidden'] ? '' : 'checked ' + content["hidden"] ? "" : "checked " end end diff --git a/app/helpers/lectures_helper.rb b/app/helpers/lectures_helper.rb index c1dda9fd3..b7d7f739e 100644 --- a/app/helpers/lectures_helper.rb +++ b/app/helpers/lectures_helper.rb @@ -10,17 +10,17 @@ def lecture_deletable?(lecture) # create text for notification about new lecture in notification dropdown menu def lecture_notification_item_header(lecture) - t('notifications.new_lecture', title: lecture.title_for_viewers) + t("notifications.new_lecture", title: lecture.title_for_viewers) end # create text for notification card - def lecture_notification_item_details(lecture) - t('notifications.subscribe_lecture') + def lecture_notification_item_details(_lecture) + t("notifications.subscribe_lecture") end # create text for notification about new course in notification card def lecture_notification_card_text(lecture) - t('notifications.new_lecture_created_html', + t("notifications.new_lecture_created_html", title: lecture.course.title, term: lecture.term_to_label, teacher: lecture.teacher.name) @@ -28,80 +28,78 @@ def lecture_notification_card_text(lecture) # create link for notification about new course in notification card def lecture_notification_card_link - t('notifications.subscribe_lecture_html', - profile: link_to(t('notifications.profile'), + t("notifications.subscribe_lecture_html", + profile: link_to(t("notifications.profile"), edit_profile_path, - class: 'darkblue')) + class: "darkblue")) end def days_short - ['Mo', 'Di', 'Mi', 'Do', 'Fr'] + ["Mo", "Di", "Mi", "Do", "Fr"] end # unpublished lecture get a different link color def lectures_color(lecture) - return '' if lecture.published? + return "" if lecture.published? - 'unpublished' + "unpublished" end # hidden chapters get a different color def chapter_card_color(chapter) - return 'bg-mdb-color-lighten-5' unless chapter.hidden + return "bg-mdb-color-lighten-5" unless chapter.hidden - 'greyed_out bg-grey' + "greyed_out bg-grey" end # hidden chapters get a different header color def chapter_header_color(chapter) - return 'bg-mdb-color-lighten-2' unless chapter.hidden + return "bg-mdb-color-lighten-2" unless chapter.hidden - '' + "" end # hidden sections get a different color def section_color(section) - return '' unless section.hidden + return "" unless section.hidden - 'greyed_out' + "greyed_out" end # hidden sections get a different background color def section_background_color(section) - unless !section.chapter.hidden && section.hidden - return 'bg-mdb-color-lighten-6' - end + return "bg-mdb-color-lighten-6" unless !section.chapter.hidden && section.hidden - 'bg-grey' + "bg-grey" end def news_color(news_count) - return '' unless news_count.positive? + return "" unless news_count.positive? - 'text-primary' + "text-primary" end def lecture_header_color(subscribed, lecture) - return '' unless subscribed + return "" unless subscribed - result = 'text-light ' - result += if lecture.term - 'bg-mdb-color-lighten-1' + result = "text-light " + result + if lecture.term + "bg-mdb-color-lighten-1" else - 'bg-info' + "bg-info" end end def circle_icon(subscribed) - return 'fas fa-check-circle' if subscribed + return "fas fa-check-circle" if subscribed - 'far fa-circle' + "far fa-circle" end def lecture_border(lecture) - return '' if lecture.published? + return "" if lecture.published? - 'border-danger' + "border-danger" end def lecture_access_icon(lecture) @@ -111,24 +109,24 @@ def lecture_access_icon(lecture) end def lecture_edit_icon(lecture) - link_to edit_lecture_path(lecture), - class: 'text-dark me-2', - style: 'text-decoration: none;', - data: { toggle: 'tooltip', - placement: 'bottom' }, - title: t('buttons.edit') do - tag.i class: 'far fa-edit' + link_to(edit_lecture_path(lecture), + class: "text-dark me-2", + style: "text-decoration: none;", + data: { toggle: "tooltip", + placement: "bottom" }, + title: t("buttons.edit")) do + tag.i(class: "far fa-edit") end end def lecture_view_icon(lecture) - link_to lecture_path(lecture), - class: 'text-dark me-2', - style: 'text-decoration: none;', - data: { toggle: 'tooltip', - placement: 'bottom' }, - title: t('buttons.view') do - tag.i class: 'fas fa-eye' + link_to(lecture_path(lecture), + class: "text-dark me-2", + style: "text-decoration: none;", + data: { toggle: "tooltip", + placement: "bottom" }, + title: t("buttons.view")) do + tag.i(class: "fas fa-eye") end end end diff --git a/app/helpers/lessons_helper.rb b/app/helpers/lessons_helper.rb index 0653e9207..da138045d 100644 --- a/app/helpers/lessons_helper.rb +++ b/app/helpers/lessons_helper.rb @@ -4,7 +4,7 @@ module LessonsHelper # tags, all given by label, together with their ids. # Is used in options_for_select in form helpers. def lesson_tag_selection(lesson) - lesson.section_tags.map { |t| t.extended_title_id_hash } + lesson.section_tags.map(&:extended_title_id_hash) .map { |t| [t[:title], t[:id]] } end diff --git a/app/helpers/media_helper.rb b/app/helpers/media_helper.rb index 711a57dfa..6cc9c5140 100644 --- a/app/helpers/media_helper.rb +++ b/app/helpers/media_helper.rb @@ -14,15 +14,15 @@ def medium_editor?(medium) end def video_download_file(medium) - medium.title + '.mp4' + "#{medium.title}.mp4" end def manuscript_download_file(medium) - medium.title + '.pdf' + "#{medium.title}.pdf" end def geogebra_download_file(medium) - medium.title + '.ggb' + "#{medium.title}.ggb" end def inspect_or_edit_medium_path(medium, inspection) @@ -33,7 +33,7 @@ def inspect_or_edit_medium_path(medium, inspection) def medium_notification_item_header(medium) return unless medium.proper? - t('notifications.new_medium_in') + medium.scoped_teachable_title + t("notifications.new_medium_in") + medium.scoped_teachable_title end def medium_notification_item_details(medium) @@ -43,20 +43,18 @@ def medium_notification_item_details(medium) # create text for notification about new medium in notification card def medium_notification_card_header(medium) teachable = medium.teachable - if teachable.media_scope.class.to_s == 'Course' - return teachable.media_scope.title_for_viewers - end + return teachable.media_scope.title_for_viewers if teachable.media_scope.instance_of?(::Course) link_to(teachable.media_scope.title_for_viewers, medium.teachable.media_scope.path(current_user), - class: 'text-dark') + class: "text-dark") end # create link to medium in notification card def medium_notification_card_link(medium) link_to(medium.local_title_for_viewers, medium.becomes(Medium), - class: 'darkblue') + class: "darkblue") end def section_selection(medium) @@ -64,62 +62,60 @@ def section_selection(medium) end def preselected_sections(medium) - return [] unless medium.teachable.class.to_s == 'Lesson' + return [] unless medium.teachable.instance_of?(::Lesson) medium.teachable.sections.map(&:id) end def textcolor(medium) - return '' if medium.visible? - return 'locked' if medium.locked? - return 'scheduled_release' if medium.publisher.present? + return "" if medium.visible? + return "locked" if medium.locked? + return "scheduled_release" if medium.publisher.present? - 'unpublished' + "unpublished" end def infotainment(medium) - return 'nichts' unless medium.video || medium.manuscript - return 'ein Video' unless medium.manuscript - return 'ein Manuskript' unless medium.video + return "nichts" unless medium.video || medium.manuscript + return "ein Video" unless medium.manuscript + return "ein Manuskript" unless medium.video - 'ein Video und ein Manuskript' + "ein Video und ein Manuskript" end def level_to_word(medium) - return t('basics.not_set') unless medium.level.present? - return t('basics.level_easy') if medium.level == 0 - return t('basics.level_medium') if medium.level == 1 + return t("basics.not_set") if medium.level.blank? + return t("basics.level_easy") if medium.level.zero? + return t("basics.level_medium") if medium.level == 1 - t('basics.level_hard') + t("basics.level_hard") end def independent_to_word(medium) - return t('basics.no_lc') unless medium.independent + return t("basics.no_lc") unless medium.independent - t('basics.yes_lc') + t("basics.yes_lc") end def medium_border(medium) return if medium.published? && !medium.locked? - 'border-danger' + "border-danger" end def media_sorts_select(purpose) - return add_prompt(Medium.select_quizzables) if purpose == 'quiz' - return Medium.select_question if purpose == 'clicker' - return add_prompt(Medium.select_importables) if purpose == 'import' - unless current_user.admin_or_editor? - return add_prompt(Medium.select_generic) - end + return add_prompt(Medium.select_quizzables) if purpose == "quiz" + return Medium.select_question if purpose == "clicker" + return add_prompt(Medium.select_importables) if purpose == "import" + return add_prompt(Medium.select_generic) unless current_user.admin_or_editor? add_prompt(Medium.select_sorts) end def sort_preselect(purpose) - return '' unless purpose == 'quiz' + return "" unless purpose == "quiz" - 'Question' + "Question" end def related_media_hash(references, media) @@ -128,15 +124,15 @@ def related_media_hash(references, media) hash = {} Medium.sort_enum.each do |s| media_in_s = media_list.select { |m| m.first.sort == s } - hash[s] = media_in_s unless media_in_s.blank? + hash[s] = media_in_s if media_in_s.present? end hash end def release_date_info(medium) - return unless medium.publisher.present? + return if medium.publisher.blank? - t('admin.medium.scheduled_for_release_short', + t("admin.medium.scheduled_for_release_short", release_date: I18n.l(medium.publisher&.release_date, format: :long, locale: I18n.locale)) @@ -151,7 +147,6 @@ def edit_or_show_medium_path(medium) def external_link_description_not_empty(medium) # Uses link display name if not empty, otherwise falls back to the # link url itself. - return medium.external_link_description.blank?\ - ? medium.external_reference_link : medium.external_link_description + (medium.external_link_description.presence || medium.external_reference_link) end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 74ff9883c..be7160ec2 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -3,7 +3,7 @@ module NotificationsHelper # create text for notification in notification dropdown menu def notification_menu_item_header(notification) notifiable = notification.notifiable - return '' unless notifiable + return "" unless notifiable return medium_notification_item_header(notifiable) if notification.medium? return course_notification_item_header(notifiable) if notification.course? return lecture_notification_item_header(notifiable) if notification.lecture? @@ -16,58 +16,54 @@ def notification_menu_item_details(notification) notifiable = notification.notifiable return medium_notification_item_details(notifiable) if notification.medium? return course_notification_item_details(notifiable) if notification.course? - if notification.lecture? - return lecture_notification_item_details(notifiable) - end + return lecture_notification_item_details(notifiable) if notification.lecture? - '' + "" end # determine the color of a notification card def notification_color(notification) - return 'bg-post-it-blue' if notification.generic_announcement? - return 'bg-post-it-red' if notification.announcement? - return 'bg-post-it-orange' if notification.course? || notification.lecture? + return "bg-post-it-blue" if notification.generic_announcement? + return "bg-post-it-red" if notification.announcement? + return "bg-post-it-orange" if notification.course? || notification.lecture? - 'bg-post-it-yellow' + "bg-post-it-yellow" end # provide text or link for header of notification card def notification_header(notification) notifiable = notification.notifiable - text = if notification.medium? + if notification.medium? medium_notification_card_header(notifiable) elsif notification.course? || notification.lecture? - t('notifications.course_selection') + t("notifications.course_selection") elsif notification.lecture_announcement? announcement_notification_card_header(notifiable) else - link_to t('mampf_news.title'), news_path, class: 'text-dark' + link_to(t("mampf_news.title"), news_path, class: "text-dark") end - text.html_safe end # provide text for body of notification card def notification_text(notification) notifiable = notification.notifiable - text = if notification.medium? - t('notifications.new_medium') + if notification.medium? + t("notifications.new_medium") elsif notification.course? course_notification_card_text(notifiable) elsif notification.lecture? lecture_notification_card_text(notifiable) else - t('notifications.new_announcement') + t("notifications.new_announcement") end - text.html_safe end # provide link for body of notification card def notification_link(notification) notifiable = notification.notifiable - return '' unless notifiable + return "" unless notifiable - text = if notification.medium? + if notification.medium? medium_notification_card_link(notifiable) elsif notification.course? course_notification_card_link @@ -76,13 +72,12 @@ def notification_link(notification) else notifiable.details end - text.html_safe end def items_card_size(small, comments_below) - return '30vh' if comments_below - return '60vh' if small + return "30vh" if comments_below + return "60vh" if small - '70vh' + "70vh" end end diff --git a/app/helpers/quizzes_helper.rb b/app/helpers/quizzes_helper.rb index 99c3ed98d..6a47eca0b 100644 --- a/app/helpers/quizzes_helper.rb +++ b/app/helpers/quizzes_helper.rb @@ -1,27 +1,29 @@ # Quizzes Helper module QuizzesHelper + # rubocop:disable Rails/HelperInstanceVariable def answer_id(a_id, progress = @quiz_round.progress, vertex = @quiz_round.vertex) - 'r' + progress.to_s + 'q' + vertex[:id].to_s + 'a' + a_id.to_s + "r#{progress}q#{vertex[:id]}a#{a_id}" end def quiz_id(q_id = @quiz_round.quiz.id) - 'quiz' + q_id.to_s + "quiz#{q_id}" end def result_id(a_id, progress = @quiz_round.progress, vertex = @quiz_round.vertex) - 'result' + progress.to_s + 'q' + vertex[:id].to_s + 'a' + a_id.to_s + "result#{progress}q#{vertex[:id]}a#{a_id}" end def cross_id(a_id, progress = @quiz_round.progress, vertex = @quiz_round.vertex) - 'cross' + progress.to_s + 'q' + vertex[:id].to_s + 'a' + a_id.to_s + "cross#{progress}q#{vertex[:id]}a#{a_id}" end + # rubocop:enable Rails/HelperInstanceVariable def vertices_labels_no_end(quiz) - special = [[I18n.t('admin.quiz.undefined'), 0]] + special = [[I18n.t("admin.quiz.undefined"), 0]] list = quiz.vertices.keys.collect { |k| [vertex_label(quiz, k), k] } - special.concat list + special.concat(list) end end diff --git a/app/helpers/referrals_helper.rb b/app/helpers/referrals_helper.rb index fe8eef939..b003d8112 100644 --- a/app/helpers/referrals_helper.rb +++ b/app/helpers/referrals_helper.rb @@ -4,17 +4,15 @@ module ReferralsHelper # in the form required by the teachable selector in the referral form, # e.g. as 'Lecture-42', 'Course-5' etc. def teachable_selector(referral) - return '' unless referral.medium.present? - unless referral.item.present? - return referral.medium.teachable&.media_scope&.selector_value - end - return 'external-0' if referral.item.sort == 'link' + return "" if referral.medium.blank? + return referral.medium.teachable&.media_scope&.selector_value if referral.item.blank? + return "external-0" if referral.item.sort == "link" referral.item.medium.teachable&.media_scope&.selector_value end def show_link(referral) - return true if referral.item.present? && referral.item.sort == 'link' + return true if referral.item.present? && referral.item.sort == "link" false end @@ -29,19 +27,19 @@ def show_explanation(referral) # (pink if the item belongs to a unpublished or locked medium, white otherwise) def item_status_color(referral) - return '' if referral.item.sort == 'link' + return "" if referral.item.sort == "link" if !referral.item_published? || referral.item_locked? || referral.item.quarantine - return 'bg-post-it-pink' + return "bg-post-it-pink" end - '' + "" end def item_status_color_value(referral) - return 'white' if referral.item.sort == 'link' - return '#fad1df' if !referral.item_published? || referral.item_locked? + return "white" if referral.item.sort == "link" + return "#fad1df" if !referral.item_published? || referral.item_locked? - 'white' + "white" end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index e1e5d90c4..c0fe05188 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -1,11 +1,11 @@ # Search Helper -require 'fuzzystringmatch' +require "fuzzystringmatch" # if more than one matching tag was omitted letter, # add letter 'n' to 'wurde' module SearchHelper def plural_n(tags, filtered_tags) - (tags.count - filtered_tags.count) > 1 ? 'n' : '' + (tags.count - filtered_tags.count) > 1 ? "n" : "" end def hits_per_page(results_as_list) diff --git a/app/helpers/sections_helper.rb b/app/helpers/sections_helper.rb index 13b8d4eb3..58d089bea 100644 --- a/app/helpers/sections_helper.rb +++ b/app/helpers/sections_helper.rb @@ -8,12 +8,12 @@ def lecture_chapters_for_select(section) end def section_positions_for_select(section) - [[t('basics.at_the_beginning'), 0]] + section.chapter.select_sections - + [[t("basics.at_the_beginning"), 0]] + section.chapter.select_sections - [[section.to_label, section.position]] end def new_section_position_for_select(chapter) - [[t('basics.at_beginning_of_chapter'), 0]] + chapter.select_sections + [[t("basics.at_beginning_of_chapter"), 0]] + chapter.select_sections end def section_lessons_for_select(section) diff --git a/app/helpers/submissions_helper.rb b/app/helpers/submissions_helper.rb index b44505b31..480418fd3 100644 --- a/app/helpers/submissions_helper.rb +++ b/app/helpers/submissions_helper.rb @@ -14,7 +14,7 @@ def partner_preselection(user, lecture) user.recent_submission_partners(lecture).map(&:id) end - def admissible_invitee_selection(user, submission, lecture) + def admissible_invitee_selection(user, submission, _lecture) submission.admissible_invitees(user).map { |u| [u.tutorial_name, u.id] } end @@ -33,70 +33,67 @@ def invitations_possible?(submission, user) def submission_color(submission, assignment) if assignment.active? - return 'bg-submission-green' if submission&.manuscript - return 'bg-submission-yellow' if submission + return "bg-submission-green" if submission&.manuscript + return "bg-submission-yellow" if submission - return 'bg-submission-red' else - return 'bg-submission-darker-green' if submission&.correction + return "bg-submission-darker-green" if submission&.correction - if submission&.manuscript && submission.too_late? - return 'bg-submission-orange' if submission.accepted.nil? - return 'bg-submission-green' if submission.accepted + if submission&.manuscript && submission&.too_late? + return "bg-submission-orange" if submission.accepted.nil? + return "bg-submission-green" if submission.accepted - return 'bg-submission-red' + return "bg-submission-red" end - return 'bg-submission-green' if submission&.manuscript + return "bg-submission-green" if submission&.manuscript - return 'bg-submission-red' end + "bg-submission-red" end def submission_status_icon(submission, assignment) if assignment.active? - return 'far fa-smile' if submission&.manuscript + return "far fa-smile" if submission&.manuscript - return 'fas fa-exclamation-triangle' else - return 'far fa-smile' if submission&.correction + return "far fa-smile" if submission&.correction - if submission&.manuscript && submission.too_late? - return 'fas fa-hourglass-start' if submission.accepted + if submission&.manuscript && submission&.too_late? + return "fas fa-hourglass-start" if submission.accepted - return 'fas fa-exclamation-triangle' + return "fas fa-exclamation-triangle" end - return 'fas fa-hourglass-start' if submission&.manuscript + return "fas fa-hourglass-start" if submission&.manuscript - return 'fas fa-exclamation-triangle' end + "fas fa-exclamation-triangle" end def submission_status_text(submission, assignment) if assignment.active? - return t('submission.okay') if submission&.manuscript - return t('submission.no_file') if submission + return t("submission.okay") if submission&.manuscript - return t('submission.nothing') else - return t('submission.with_correction') if submission&.correction + return t("submission.with_correction") if submission&.correction - if submission&.manuscript && submission.too_late? - return t('submission.too_late') if submission.accepted.nil? - return t('submission.too_late_accepted') if submission.accepted + if submission&.manuscript && submission&.too_late? + return t("submission.too_late") if submission.accepted.nil? + return t("submission.too_late_accepted") if submission.accepted - return t('submission.too_late_rejected') + return t("submission.too_late_rejected") end - return t('submission.under_review') if submission&.manuscript - return t('submission.no_file') if submission + return t("submission.under_review") if submission&.manuscript - return t('submission.nothing') end + return t("submission.no_file") if submission + + t("submission.nothing") end def submission_status(submission, assignment) - tag.i class: [submission_status_icon(submission, assignment), 'fa-lg'], - data: { toggle: 'tooltip' }, - title: submission_status_text(submission, assignment) + tag.i(class: [submission_status_icon(submission, assignment), "fa-lg"], + data: { toggle: "tooltip" }, + title: submission_status_text(submission, assignment)) end def show_submission_footer?(submission, assignment) @@ -108,24 +105,24 @@ def show_submission_footer?(submission, assignment) end def submission_late_color(submission) - return '' unless submission.too_late? - return '' unless submission.accepted.nil? + return "" unless submission.too_late? + return "" unless submission.accepted.nil? - 'bg-submission-orange' + "bg-submission-orange" end def late_submission_info(submission, tutorial) - text = t('submission.late') + text = t("submission.late") return text unless submission.accepted.nil? && current_user.in?(tutorial.tutors) - "#{text} (#{t('tutorial.late_submission_decision')})" + "#{text} (#{t("tutorial.late_submission_decision")})" end def correction_display_mode(submission) accepted = submission.assignment.accepted_file_type non_inline = Assignment.non_inline_file_types - return t('buttons.show') unless accepted.in?(non_inline) + return t("buttons.show") unless accepted.in?(non_inline) - t('buttons.download') + t("buttons.download") end end diff --git a/app/helpers/talks_helper.rb b/app/helpers/talks_helper.rb index 73a2fcc22..8f17c847b 100644 --- a/app/helpers/talks_helper.rb +++ b/app/helpers/talks_helper.rb @@ -1,45 +1,45 @@ # Talks Helper module TalksHelper def talk_positions_for_select(talk) - [[t('basics.at_the_beginning'), 0]] + talk.lecture.select_talks - + [[t("basics.at_the_beginning"), 0]] + talk.lecture.select_talks - [[talk.to_label, talk.position]] end def talk_card_color(talk, user) - return 'bg-mdb-color-lighten-2' unless user.in?(talk.speakers) + return "bg-mdb-color-lighten-2" unless user.in?(talk.speakers) - 'bg-info' + "bg-info" end def speaker_list(talk) - return t('basics.tba') unless talk.speakers.present? + return t("basics.tba") if talk.speakers.blank? - talk.speakers.map(&:tutorial_name).join(', ') + talk.speakers.map(&:tutorial_name).join(", ") end def speaker_icon_class(talk) - return 'fas fa-user' unless talk.speakers.count > 1 + return "fas fa-user" unless talk.speakers.count > 1 - 'fas fa-users' + "fas fa-users" end def speaker_icon(talk) content_tag(:i, - '', + "", class: "#{speaker_icon_class(talk)} me-2", - data: { toggle: 'tooltip' }, - title: t('admin.talk.speakers')).html_safe + data: { toggle: "tooltip" }, + title: t("admin.talk.speakers")).html_safe end def speaker_list_with_icon(talk) - (speaker_icon(talk) + speaker_list(talk)).html_safe + speaker_icon(talk) + speaker_list(talk) end def date_list(talk) - talk.dates.map { |d| I18n.l(d) }.join(', ') + talk.dates.map { |d| I18n.l(d) }.join(", ") end def cospeaker_list(talk, user) - (talk.speakers.to_a - [user]).map(&:tutorial_name).join(', ') + (talk.speakers.to_a - [user]).map(&:tutorial_name).join(", ") end end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 1918ef519..0e92f4b15 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -6,7 +6,7 @@ def users_for_select(users) def select_proper_teaching_related_lectures(user) user.proper_teaching_related_lectures - .sort_by { |l| [l.begin_date.to_time.to_i * (-1), l.title] } + .sort_by { |l| [l.begin_date.to_time.to_i * -1, l.title] } .map { |l| [l.title, l.id] } end end diff --git a/app/helpers/vertices_helper.rb b/app/helpers/vertices_helper.rb index db0401c48..a02c7cf89 100644 --- a/app/helpers/vertices_helper.rb +++ b/app/helpers/vertices_helper.rb @@ -1,12 +1,12 @@ # Vertices Helper module VerticesHelper def vertices_labels(quiz, vertex_id, undefined) - special = [[undefined ? I18n.t('admin.quiz.undefined') : I18n.t('admin.quiz.default'), 0], - [I18n.t('admin.quiz.end'), -1]] + special = [[undefined ? I18n.t("admin.quiz.undefined") : I18n.t("admin.quiz.default"), 0], + [I18n.t("admin.quiz.end"), -1]] list = (quiz.vertices.keys - [vertex_id]).collect do |k| [quiz.quizzable(k).label, k] end - special.concat list + special.concat(list) end def crosses_id(crosses) diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 5c9da133f..0586df987 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -7,7 +7,6 @@ // To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate // layout file, like app/views/layouts/application.html.erb - // Uncomment to copy all static images under ../images to the output folder and reference // them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>) // or the `imagePath` JavaScript helper below. @@ -16,40 +15,39 @@ // const imagePath = (name) => images(name, true) import { - WidgetInstance + WidgetInstance, } from "friendly-challenge"; -var friendlyChallengeWidgetInstance = WidgetInstance document.addEventListener("turbolinks:load", function () { - var doneCallback, element, options, widget; + var doneCallback, element, options; - doneCallback = function (solution) { - console.log(solution); - document.querySelector("#register-user").disabled = false; + doneCallback = function (solution) { + console.log(solution); + document.querySelector("#register-user").disabled = false; + }; + const errorCallback = (err) => { + console.log("There was an error when trying to solve the Captcha."); + console.log(err); + }; + element = document.querySelector("#captcha-widget"); + if (element != null) { + options = { + doneCallback: doneCallback, + errorCallback, + puzzleEndpoint: $("#captcha-widget").data("captcha-url"), + startMode: "auto", + language: $("#captcha-widget").data("lang"), }; - const errorCallback = (err) => { - console.log('There was an error when trying to solve the Captcha.'); - console.log(err); - } - element = document.querySelector('#captcha-widget'); - if (element != null) { - options = { - doneCallback: doneCallback, - errorCallback, - puzzleEndpoint: $('#captcha-widget').data("captcha-url"), - startMode: "auto", - language: $('#captcha-widget').data("lang") - }; - console.log(options) - widget = new WidgetInstance(element, options); - //DO not uncomment, evil - // widget.reset(); - } + console.log(options); + new WidgetInstance(element, options); + // DO not uncomment, evil + // widget.reset(); + } - // Init Masonry grid system - // see https://getbootstrap.com/docs/5.0/examples/masonry/ - // and official documentation: https://masonry.desandro.com/ - $('.masonry-grid').masonry({ - percentPosition: true - }); -}) \ No newline at end of file + // Init Masonry grid system + // see https://getbootstrap.com/docs/5.0/examples/masonry/ + // and official documentation: https://masonry.desandro.com/ + $(".masonry-grid").masonry({ + percentPosition: true, + }); +}); diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index d8255248e..61fa90cb9 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,8 +1,10 @@ class ApplicationMailer < ActionMailer::Base helper EmailHelper default from: DefaultSetting::PROJECT_EMAIL - default "Message-ID" => -> { - "<#{rand.to_s.split('.')[1]}.#{Time.now.to_i}@#{ENV['MAILID_DOMAIN']}>" + default "Message-ID" => lambda { + "<#{rand.to_s.split(".")[1]}.#{Time.now.to_i}@#{ENV.fetch( + "MAILID_DOMAIN", nil + )}>" } - layout 'mailer' + layout "mailer" end diff --git a/app/mailers/exception_handler/exception_mailer.rb b/app/mailers/exception_handler/exception_mailer.rb index aa1913e52..84db08e96 100644 --- a/app/mailers/exception_handler/exception_mailer.rb +++ b/app/mailers/exception_handler/exception_mailer.rb @@ -1,19 +1,19 @@ module ExceptionHandler - class ExceptionMailer < ActionMailer::Base + class ExceptionMailer < ApplicationMailer # Layout layout "exception_mailer" # Defaults - default subject: I18n.t('exception.exception', - host: ENV['URL_HOST']) + default subject: I18n.t("exception.exception", + host: ENV.fetch("URL_HOST", nil)) default from: ExceptionHandler.config.email default template_path: "exception_handler/mailers" # => http://stackoverflow.com/a/18579046/1143732 - def new_exception e - @exception = e - mail to: ExceptionHandler.config.email - Rails.logger.info "Exception Sent To → #{ExceptionHandler.config.email}" + def new_exception(err) + @exception = err + mail(to: ExceptionHandler.config.email) + Rails.logger.info("Exception Sent To → #{ExceptionHandler.config.email}") end end end diff --git a/app/mailers/mathi_mailer.rb b/app/mailers/mathi_mailer.rb index 438fbe30e..de9a71172 100644 --- a/app/mailers/mathi_mailer.rb +++ b/app/mailers/mathi_mailer.rb @@ -7,20 +7,20 @@ def ghost_email(user) @name = user.name @hash = user.ghost_hash - mail(to: user.email, subject: t('mailer.hash_mail_subject')) + mail(to: user.email, subject: t("mailer.hash_mail_subject")) end def data_request_email(user) @mail = user.email @id = user.id - mail(to: DefaultSetting::PROJECT_EMAIL, subject: 'Data request') + mail(to: DefaultSetting::PROJECT_EMAIL, subject: t("mailer.data_provide_mail_subject")) end def data_provide_email(user) @user = user mail(to: user.email, - subject: t('mailer.data_provide_mail_subject')) do |format| - format.html { render layout: 'mailer' } + subject: t("mailer.data_provide_mail_subject")) do |format| + format.html { render layout: "mailer" } end end end diff --git a/app/mailers/my_mailer.rb b/app/mailers/my_mailer.rb index c57e0eb04..5d7e1ced2 100644 --- a/app/mailers/my_mailer.rb +++ b/app/mailers/my_mailer.rb @@ -2,10 +2,12 @@ class MyMailer < Devise::Mailer helper :application # gives access to all helpers defined within `application_helper`. include Devise::Controllers::UrlHelpers # Optional. eg. `confirmation_url` layout "devise_mailer" - default template_path: 'devise/mailer' # to make sure that your mailer uses the devise views + default template_path: "devise/mailer" # to make sure that your mailer uses the devise views default from: DefaultSetting::PROJECT_EMAIL - default "Message-ID" => -> { - "<#{rand.to_s.split('.')[1]}.#{Time.now.to_i}@#{ENV['MAILID_DOMAIN']}>" + default "Message-ID" => lambda { + "<#{rand.to_s.split(".")[1]}.#{Time.now.to_i}@#{ENV.fetch( + "MAILID_DOMAIN", nil + )}>" } helper EmailHelper end diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 03a4419b0..ae6a281c1 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -24,31 +24,30 @@ class NotificationMailer < ApplicationMailer def medium_email @medium = params[:medium] + subject = t("mailer.medium_subject") + viewer_title = @medium.teachable.media_scope.title_for_viewers mail(from: @sender, bcc: @recipients.pluck(:email), - subject: t('mailer.medium_subject') + ' ' + t('in') + ' ' + - @medium.teachable.media_scope.title_for_viewers) + subject: "#{subject} #{t("in")} #{viewer_title}") end def announcement_email @announcement = params[:announcement] @announcement_details = if @announcement.lecture.present? - t('in') + ' ' + - @announcement.lecture.title_for_viewers + "#{t("in")} #{@announcement.lecture.title_for_viewers}" else - t('mailer.mampf_news') + t("mailer.mampf_news") end mail(from: @sender, bcc: @recipients.pluck(:email), - subject: t('mailer.announcement_subject') + ' ' + - @announcement_details) + subject: "#{t("mailer.announcement_subject")} #{@announcement_details}") end def new_lecture_email @lecture = params[:lecture] mail(from: @sender, bcc: @recipients.pluck(:email), - subject: t('mailer.new_lecture_subject', + subject: t("mailer.new_lecture_subject", title: @lecture.title_for_viewers)) end @@ -59,7 +58,7 @@ def new_editor_email mail(from: @sender, to: @recipient.email, - subject: t('mailer.new_editor_subject', + subject: t("mailer.new_editor_subject", title: @lecture.title_for_viewers)) end @@ -70,7 +69,7 @@ def submission_invitation_email @issuer = params[:issuer] mail(from: @sender, to: @recipient.email, - subject: t('mailer.submission_invitation_subject', + subject: t("mailer.submission_invitation_subject", assignment: @assignment.title, lecture: @assignment.lecture.short_title)) end @@ -79,7 +78,7 @@ def submission_upload_email @uploader = params[:uploader] mail(from: @sender, to: @recipient.email, - subject: t('mailer.submission_upload_subject', + subject: t("mailer.submission_upload_subject", assignment: @assignment.title, lecture: @assignment.lecture.short_title)) end @@ -88,7 +87,7 @@ def submission_upload_removal_email @remover = params[:remover] mail(from: @sender, to: @recipient.email, - subject: t('mailer.submission_upload_removal_subject', + subject: t("mailer.submission_upload_removal_subject", assignment: @assignment.title, lecture: @assignment.lecture.short_title)) end @@ -96,7 +95,7 @@ def submission_upload_removal_email def submission_join_email mail(from: @sender, to: @recipient.email, - subject: t('mailer.submission_join_subject', + subject: t("mailer.submission_join_subject", assignment: @assignment.title, lecture: @assignment.lecture.short_title, user: @user.tutorial_name)) @@ -105,7 +104,7 @@ def submission_join_email def submission_leave_email mail(from: @sender, to: @recipient.email, - subject: t('mailer.submission_leave_subject', + subject: t("mailer.submission_leave_subject", assignment: @assignment.title, lecture: @assignment.lecture.short_title, user: @user.tutorial_name)) @@ -115,7 +114,7 @@ def correction_upload_email @tutor = params[:tutor] mail(from: @sender, to: @recipient.email, - subject: t('mailer.correction_upload_subject', + subject: t("mailer.correction_upload_subject", assignment: @assignment.title, lecture: @assignment.lecture.short_title)) end @@ -123,7 +122,7 @@ def correction_upload_email def submission_acceptance_email mail(from: @sender, to: @recipient.email, - subject: t('mailer.submission_acceptance_subject', + subject: t("mailer.submission_acceptance_subject", assignment: @assignment.title, lecture: @assignment.lecture.short_title)) end @@ -131,7 +130,7 @@ def submission_acceptance_email def submission_rejection_email mail(from: @sender, to: @recipient.email, - subject: t('mailer.submission_rejection_subject', + subject: t("mailer.submission_rejection_subject", assignment: @assignment.title, lecture: @assignment.lecture.short_title)) end @@ -139,8 +138,8 @@ def submission_rejection_email def submission_deletion_email @deletion_date = params[:deletion_date] @lectures = params[:lectures] - subject = params[:reminder] ? t('basics.reminder') + ': ' : '' - subject += t('mailer.submission_deletion_subject') + subject = params[:reminder] ? "#{t("basics.reminder")}: " : "" + subject += t("mailer.submission_deletion_subject") mail(from: @sender, bcc: @recipients.pluck(:email), subject: subject) @@ -149,8 +148,8 @@ def submission_deletion_email def submission_deletion_lecture_email @lecture = params[:lecture] @deletion_date = params[:deletion_date] - subject = params[:reminder] ? t('basics.reminder') + ': ' : '' - subject += t('mailer.submission_deletion_lecture_subject', + subject = params[:reminder] ? "#{t("basics.reminder")}: " : "" + subject += t("mailer.submission_deletion_lecture_subject", lecture: @lecture.title) mail(from: @sender, bcc: @recipients.pluck(:email), @@ -161,8 +160,8 @@ def submission_destruction_email @deletion_date = params[:deletion_date] mail(from: @sender, bcc: @recipients.pluck(:email), - subject: t('mailer.submission_destruction_subject', - deletion_date: @deletion_date.strftime(I18n.t('date.formats.concise')))) + subject: t("mailer.submission_destruction_subject", + deletion_date: @deletion_date.strftime(I18n.t("date.formats.concise")))) end def submission_destruction_lecture_email @@ -170,14 +169,14 @@ def submission_destruction_lecture_email @deletion_date = params[:deletion_date] mail(from: @sender, bcc: @recipients.pluck(:email), - subject: t('mailer.submission_destruction_lecture_subject', + subject: t("mailer.submission_destruction_lecture_subject", lecture: @lecture.title)) end private def set_sender_and_locale - @sender = "#{t('mailer.notification')} <#{DefaultSetting::PROJECT_NOTIFICATION_EMAIL}>" + @sender = "#{t("mailer.notification")} <#{DefaultSetting::PROJECT_NOTIFICATION_EMAIL}>" I18n.locale = params[:locale] end diff --git a/app/models/ability.rb b/app/models/ability.rb index fbf76c958..26dd1134b 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -5,7 +5,4 @@ class Ability # See the wiki for details: # https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities include CanCan::Ability - - def initialize(user) - end end diff --git a/app/models/announcement.rb b/app/models/announcement.rb index 04d8eae44..4504b0ff7 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -2,7 +2,7 @@ class Announcement < ApplicationRecord # changing an announcement needs to make the lecture cache key expire belongs_to :lecture, optional: true, touch: true - belongs_to :announcer, class_name: 'User' + belongs_to :announcer, class_name: "User" validates :details, presence: true @@ -13,6 +13,6 @@ class Announcement < ApplicationRecord # does there (still) exist a notification for the announcement for # the given user def active?(user) - user.notifications.where(notifiable: self).exists? + user.notifications.exists?(notifiable: self) end end diff --git a/app/models/answer.rb b/app/models/answer.rb index 7bc2403a1..ea407d192 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -1,19 +1,15 @@ class Answer < ApplicationRecord belongs_to :question, touch: true + after_create :update_quizzes before_destroy :question_not_orphaned? after_destroy :update_quizzes - after_create :update_quizzes after_save :touch_medium def conditional_explanation(correct) - unless /\(korrekt:.*\):\(inkorrekt:.*\)/.match?(explanation) - return explanation - end - unless correct - return explanation.string_between_markers(':(inkorrekt:', ')') - end + return explanation unless /\(korrekt:.*\):\(inkorrekt:.*\)/.match?(explanation) + return explanation.string_between_markers(":(inkorrekt:", ")") unless correct - explanation.string_between_markers('(korrekt:', '):') + explanation.string_between_markers("(korrekt:", "):") end def duplicate(new_question) @@ -45,6 +41,6 @@ def update_quizzes end def touch_medium - question.becomes(Medium).update(updated_at: Time.now) + question.becomes(Medium).touch end end diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 0da21a6ad..40f143d54 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -5,7 +5,9 @@ class Assignment < ApplicationRecord before_destroy :check_destructibility, prepend: true + # rubocop:todo Rails/UniqueValidationWithoutIndex validates :title, uniqueness: { scope: [:lecture_id] }, presence: true + # rubocop:enable Rails/UniqueValidationWithoutIndex validates :deadline, presence: true validates :deletion_date, presence: true validate :deletion_date_cannot_be_in_the_past @@ -13,17 +15,17 @@ class Assignment < ApplicationRecord def deletion_date_cannot_be_in_the_past return unless deletion_date.present? && deletion_date < Time.zone.now.to_date - errors.add(:deletion_date, I18n.t('activerecord.errors.models.' \ - 'assignment.attributes.deletion_date.' \ - 'in_past')) + errors.add(:deletion_date, I18n.t("activerecord.errors.models." \ + "assignment.attributes.deletion_date." \ + "in_past")) end - scope :active, -> { where('deadline >= ?', Time.now) } + scope :active, -> { where("deadline >= ?", Time.zone.now) } - scope :expired, -> { where('deadline < ?', Time.now) } + scope :expired, -> { where("deadline < ?", Time.zone.now) } def self.accepted_file_types - ['.pdf', '.tar.gz', '.cc', '.hh', '.m', '.mlx', '.zip'] + [".pdf", ".tar.gz", ".cc", ".hh", ".m", ".mlx", ".zip"] end validates :accepted_file_type, @@ -32,7 +34,7 @@ def self.accepted_file_types def submission(user) UserSubmissionJoin.where(submission: Submission.where(assignment: self), user: user) - &.first&.submission + &.first&.submission end def submitter_ids @@ -44,11 +46,11 @@ def submitters end def active? - Time.now <= deadline + Time.zone.now <= deadline end def semiactive? - Time.now <= friendly_deadline + Time.zone.now <= friendly_deadline end def expired? @@ -70,11 +72,11 @@ def friendly_deadline end def current? - self.in?(lecture.current_assignments) + in?(lecture.current_assignments) end def previous? - self.in?(lecture.previous_assignments) + in?(lecture.previous_assignments) end def previous @@ -105,36 +107,36 @@ def check_destructibility true end - def has_documents? + def documents? return false unless medium medium.video || medium.manuscript || medium.geogebra || medium.external_reference_link.present? || - (medium.sort == 'Quiz' && medium.quiz_graph) + (medium.sort == "Quiz" && medium.quiz_graph) end def self.accepted_mime_types - { '.pdf' => ['application/pdf'], - '.tar.gz' => ['application/gzip', 'application/x-gzip', - 'application/x-gunzip', 'application/gzipped', - 'application/gzip-compressed', 'application/x-compressed', - 'application/x-compress', 'gzip/document', - 'application/octet-stream'], - '.cc' => ['text/*'], - '.hh' => ['text/*'], - '.m' => ['text/*'], - '.mlx' => ['application/zip', 'application/x-zip', - 'application/x-zip-compressed', 'application/octet-stream', - 'application/x-compress', 'application/x-compressed', - 'multipart/x-zip'], - '.zip' => ['application/zip', 'application/x-zip', - 'application/x-zip-compressed', 'application/octet-stream', - 'application/x-compress', 'application/x-compressed', - 'multipart/x-zip'] } + { ".pdf" => ["application/pdf"], + ".tar.gz" => ["application/gzip", "application/x-gzip", + "application/x-gunzip", "application/gzipped", + "application/gzip-compressed", "application/x-compressed", + "application/x-compress", "gzip/document", + "application/octet-stream"], + ".cc" => ["text/*"], + ".hh" => ["text/*"], + ".m" => ["text/*"], + ".mlx" => ["application/zip", "application/x-zip", + "application/x-zip-compressed", "application/octet-stream", + "application/x-compress", "application/x-compressed", + "multipart/x-zip"], + ".zip" => ["application/zip", "application/x-zip", + "application/x-zip-compressed", "application/octet-stream", + "application/x-compress", "application/x-compressed", + "multipart/x-zip"] } end def self.non_inline_file_types - ['.tar.gz', '.zip', '.mlx'] + [".tar.gz", ".zip", ".mlx"] end def accepted_mime_types @@ -145,12 +147,12 @@ def accepted_mime_types # is set to .tar.gz # see e.g. https://bugs.chromium.org/p/chromium/issues/detail?id=521781 def accepted_for_file_input - return accepted_file_type unless accepted_file_type == '.tar.gz' + return accepted_file_type unless accepted_file_type == ".tar.gz" - '.gz' + ".gz" end def localized_deletion_date - deletion_date.strftime(I18n.t('date.formats.concise')) + deletion_date.strftime(I18n.t("date.formats.concise")) end end diff --git a/app/models/chapter.rb b/app/models/chapter.rb index cad5d882c..c94c344cf 100644 --- a/app/models/chapter.rb +++ b/app/models/chapter.rb @@ -3,12 +3,14 @@ class Chapter < ApplicationRecord belongs_to :lecture, touch: true # the chapters of a lecture form an ordered list acts_as_list scope: :lecture - has_many :sections, -> { order(position: :asc) }, dependent: :destroy + has_many :sections, -> { order(position: :asc) }, + dependent: :destroy, + inverse_of: :chapter validates :title, presence: true - after_save :touch_sections - after_save :touch_chapters before_destroy :touch_sections before_destroy :touch_chapters + after_save :touch_sections + after_save :touch_chapters def to_label unless hidden @@ -23,7 +25,7 @@ def to_label # Returns the number of the chapter. Unless the user explicitly specified # a display number, this number is calculated def displayed_number - return calculated_number unless display_number.present? + return calculated_number if display_number.blank? display_number end @@ -36,7 +38,7 @@ def reference # Returns the chapter number based on the position in the chapters list. def calculated_number - return position.to_s unless lecture.start_chapter.present? + return position.to_s if lecture.start_chapter.blank? (lecture.start_chapter + position - 1).to_s end @@ -65,18 +67,18 @@ def select_sections end def cache_key - super + '-' + I18n.locale.to_s + "#{super}-#{I18n.locale}" end def touch_chapters - lecture.chapters.update_all(updated_at: Time.now) + lecture.chapters.touch_all end def touch_sections unless lecture.absolute_numbering - sections.update_all(updated_at: Time.now) + sections.touch_all return end - Section.where(chapter: lecture.chapters).update_all(updated_at: Time.now) + Section.where(chapter: lecture.chapters).touch_all end end diff --git a/app/models/clicker.rb b/app/models/clicker.rb index 075918d04..5ba6dcaec 100644 --- a/app/models/clicker.rb +++ b/app/models/clicker.rb @@ -1,23 +1,25 @@ # Clicker class class Clicker < ApplicationRecord - belongs_to :editor, class_name: 'User' + belongs_to :editor, class_name: "User" belongs_to :question, optional: true before_create :set_basics + # rubocop:todo Rails/UniqueValidationWithoutIndex validates :title, uniqueness: { scope: [:editor_id] } + # rubocop:enable Rails/UniqueValidationWithoutIndex validates :title, presence: true - has_many :votes, dependent: :destroy, class_name: 'ClickerVote' + has_many :votes, dependent: :destroy, class_name: "ClickerVote" def user_link - clicker_url(self, host: 'localhost').gsub('clickers', 'c') + clicker_url(self, host: "localhost").gsub("clickers", "c") end def editor_link clicker_url(self, - host: 'localhost', - params: { code: code }).gsub('clickers', 'c') + host: "localhost", + params: { code: code }).gsub("clickers", "c") end def closed? diff --git a/app/models/course.rb b/app/models/course.rb index 4389c3325..5edf63bfa 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # Course class class Course < ApplicationRecord include ApplicationHelper @@ -14,7 +12,9 @@ class Course < ApplicationRecord after_remove: :touch_tag, after_add: :touch_tag - has_many :media, -> { order(position: :asc) }, as: :teachable + has_many :media, -> { order(position: :asc) }, + as: :teachable, + inverse_of: :teachable # in a course, you can import other media has_many :imports, as: :teachable, dependent: :destroy @@ -35,8 +35,12 @@ class Course < ApplicationRecord has_many :division_course_joins, dependent: :destroy has_many :divisions, through: :division_course_joins + # rubocop:todo Rails/UniqueValidationWithoutIndex validates :title, presence: true, uniqueness: true + # rubocop:enable Rails/UniqueValidationWithoutIndex + # rubocop:todo Rails/UniqueValidationWithoutIndex validates :short_title, presence: true, uniqueness: true + # rubocop:enable Rails/UniqueValidationWithoutIndex # some information about media and lectures are cached # to find out whether the cache is out of date, always touch'em after saving @@ -79,7 +83,7 @@ def talk end def selector_value - 'Course-' + id.to_s + "Course-#{id}" end def to_label @@ -123,12 +127,8 @@ def subscribable_lectures(user) return lectures.published unless user.edited_lectures.any? || user.teacher? lectures.left_outer_joins(:editable_user_joins) - .where('released IS NOT NULL OR editable_user_joins.user_id = ?'\ - ' OR teacher_id = ?', user.id, user.id).distinct - end - - def restricted? - false + .where("released IS NOT NULL OR editable_user_joins.user_id = ? " \ + "OR teacher_id = ?", user.id, user.id).distinct end def lectures_by_date @@ -279,19 +279,19 @@ def normalized_image_url_with_host def image_filename return unless image - image.metadata['filename'] + image.metadata["filename"] end def image_size return unless image - image.metadata['size'] + image.metadata["size"] end def image_resolution return unless image - "#{image.metadata['width']}x#{image.metadata['height']}" + "#{image.metadata["width"]}x#{image.metadata["height"]}" end # returns all titles of courses whose title is close to the given search @@ -306,17 +306,17 @@ def self.similar_courses(search_string) def self.search_by(search_params, page) editor_ids = search_params[:editor_ids] - editor_ids = [] if search_params[:all_editors] == '1' + editor_ids = [] if search_params[:all_editors] == "1" program_ids = search_params[:program_ids] || [] - program_ids = [] if search_params[:all_programs] == '1' + program_ids = [] if search_params[:all_programs] == "1" search = Sunspot.new_search(Course) search.build do with(:editor_ids, editor_ids) with(:program_ids, program_ids) unless program_ids.empty? - with(:term_independent, true) if search_params[:term_independent] == '1' - fulltext search_params[:fulltext] if search_params[:fulltext].present? + with(:term_independent, true) if search_params[:term_independent] == "1" + fulltext(search_params[:fulltext]) if search_params[:fulltext].present? order_by(:sort_title, :asc) - paginate page: page, per_page: search_params[:per] + paginate(page: page, per_page: search_params[:per]) end search end @@ -324,26 +324,26 @@ def self.search_by(search_params, page) private def touch_media - media_with_inheritance.update_all(updated_at: Time.now) + media_with_inheritance.touch_all end def touch_tag(tag) tag.touch - Sunspot.index! tag + Sunspot.index!(tag) end def touch_lectures_and_lessons - lectures.update_all(updated_at: Time.now) - Lesson.where(lecture: lectures).update_all(updated_at: Time.now) + lectures.touch_all + Lesson.where(lecture: lectures).touch_all end def create_quiz_by_questions!(question_ids) quiz_graph = QuizGraph.build_from_questions(question_ids) - Quiz.create(description: "#{I18n.t('categories.randomquiz.singular')} "\ - "#{course.title} #{Time.now}", + Quiz.create(description: "#{I18n.t("categories.randomquiz.singular")} " \ + "#{course.title} #{Time.current}", level: 1, quiz_graph: quiz_graph, - sort: 'RandomQuiz', + sort: "RandomQuiz", locale: locale) end @@ -351,7 +351,7 @@ def question_ids_for_quiz(tags, count) return questions_w_inheritance.pluck(:id).sample(count) unless tags.any? tagged_questions = questions(tags) - question_ids = if tagged_questions.count > count + if tagged_questions.count > count QuestionSampler.new(tagged_questions, tags, count).sample! else tagged_questions.map(&:id).shuffle diff --git a/app/models/course_self_join.rb b/app/models/course_self_join.rb index 7311f85eb..689df24ac 100644 --- a/app/models/course_self_join.rb +++ b/app/models/course_self_join.rb @@ -4,7 +4,7 @@ # is built upon class CourseSelfJoin < ApplicationRecord belongs_to :course - belongs_to :preceding_course, class_name: 'Course' + belongs_to :preceding_course, class_name: "Course" validates :preceding_course, uniqueness: { scope: :course } # we do not allow a course to be preceding itself diff --git a/app/models/course_tag_join.rb b/app/models/course_tag_join.rb index 92a30f4e2..45a05093a 100644 --- a/app/models/course_tag_join.rb +++ b/app/models/course_tag_join.rb @@ -4,11 +4,11 @@ class CourseTagJoin < ApplicationRecord belongs_to :course belongs_to :tag + before_destroy :touch_tag # tags are cached in several situations # in order to see when changes have been made, # touches are triggered after_save :touch_tag - before_destroy :touch_tag private diff --git a/app/models/import.rb b/app/models/import.rb index 7455657a1..466c6e9c3 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -2,5 +2,7 @@ class Import < ApplicationRecord belongs_to :medium belongs_to :teachable, polymorphic: true + # rubocop:todo Rails/UniqueValidationWithoutIndex validates :medium, uniqueness: { scope: [:teachable] } + # rubocop:enable Rails/UniqueValidationWithoutIndex end diff --git a/app/models/interaction.rb b/app/models/interaction.rb index 774e8e794..87e8331a2 100644 --- a/app/models/interaction.rb +++ b/app/models/interaction.rb @@ -2,11 +2,11 @@ class Interaction < InteractionsRecord scope :created_between, lambda { |start_date, end_date| where(created_at: start_date.beginning_of_day..end_date.end_of_day) } - require 'csv' + require "csv" def self.to_csv - attributes = %w{id session_id created_at full_path referrer_url - study_participant} + attributes = ["id", "session_id", "created_at", "full_path", "referrer_url", + "study_participant"] CSV.generate(headers: true) do |csv| csv << attributes diff --git a/app/models/item.rb b/app/models/item.rb index bef489836..c4778ca63 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -34,13 +34,13 @@ class Item < ApplicationRecord # (these can be generated using the \hypertarget command of # the hyperref package of LaTex) # self - corresponds to items that are just wrappers around a medium - validates :sort, inclusion: { in: ['remark', 'theorem', 'lemma', 'definition', - 'annotation', 'example', 'section', - 'algorithm', 'label', 'corollary', - 'link', 'pdf_destination', 'self', - 'proposition', 'Lemma', 'Theorem', - 'subsection', 'Corollary', 'figure', - 'chapter', 'exercise', 'equation'] } + validates :sort, inclusion: { in: ["remark", "theorem", "lemma", "definition", + "annotation", "example", "section", + "algorithm", "label", "corollary", + "link", "pdf_destination", "self", + "proposition", "Lemma", "Theorem", + "subsection", "Corollary", "figure", + "chapter", "exercise", "equation"] } validates :link, http_url: true, if: :proper_link? validates :description, presence: true, if: :link? validate :valid_start_time @@ -48,10 +48,10 @@ class Item < ApplicationRecord validate :no_duplicate_start_time validate :nonempty_link_or_explanation + before_destroy :touch_medium # media are cached in several places # items are touched in order to find out whether cache is out of date after_save :touch_medium - before_destroy :touch_medium scope :unquarantined, -> { where(quarantine: [nil, false]) } scope :content, -> { where(sort: Item.content_sorts) } @@ -70,7 +70,7 @@ def end_time # result might look like this: # "01:14:40.500 --> 01:19:42.249\n" def vtt_time_span - start_time.vtt_string + ' --> ' + end_time.vtt_string + "\n" + "#{start_time.vtt_string} --> #{end_time.vtt_string}\n" end # returns the description of the toc entry corresponding to this item @@ -78,8 +78,8 @@ def vtt_time_span # result might look like this: # "zu freien Moduln" def vtt_text - return '' if sort == 'pdf_destination' - return description if sort == 'link' + return "" if sort == "pdf_destination" + return description if sort == "link" short_description end @@ -89,9 +89,9 @@ def vtt_text # result might look like this: # "Bem. 29.13: zu freien Moduln\n\n" def vtt_reference - return short_description + "\n\n" unless short_reference.present? + return "#{short_description}\n\n" if short_reference.blank? - short_reference + ': ' + short_description + "\n\n" + "#{short_reference}: #{short_description}\n\n" end # returns a reference to the item as it is used in .vtt files, @@ -99,10 +99,10 @@ def vtt_reference # result might look like this: # "Verweis auf LA 2 SS 17, Bem. 29.13:" def vtt_meta_reference(referring_medium) - return I18n.t('item.external_reference') if sort == 'link' + return I18n.t("item.external_reference") if sort == "link" ref = local?(referring_medium) ? short_reference : long_reference - I18n.t('item.internal_reference', ref: ref) + I18n.t("item.internal_reference", ref: ref) end # creates a reference as it would look like form *within* the given context @@ -118,18 +118,18 @@ def short_reference # result might look like this: # "LA 2 SS 17, Bem. 29.13" def long_reference - return short_reference if sort.in?(['self', 'link']) + return short_reference if sort.in?(["self", "link"]) return short_ref_with_teachable if section.present? - return medium.title_for_viewers unless short_reference.present? + return medium.title_for_viewers if short_reference.blank? - medium.title_for_viewers + ', ' + short_reference + "#{medium.title_for_viewers}, #{short_reference}" end # returns just the description, unless sort is section or self # result might look like this: "zu freien Moduln" def short_description - return section.title if sort == 'section' && section.present? - return medium.title_for_viewers if sort == 'self' + return section.title if sort == "section" && section.present? + return medium.title_for_viewers if sort == "self" description.to_s end @@ -141,10 +141,10 @@ def short_description # "KaViaR, Sitzung 27 vom 17.8.2017" (self) # "extern Spiegel" (link) def local_reference - unless sort.in?(['self', 'link', 'pdf_destination']) - return short_ref_with_description unless medium&.sort == 'Script' + unless sort.in?(["self", "link", "pdf_destination"]) + return short_ref_with_description unless medium&.sort == "Script" - return 'Skript, ' + short_ref_with_description + return "Skript, #{short_ref_with_description}" end local_non_math_reference end @@ -153,11 +153,11 @@ def local_reference # Result might look like this: # "SS 17, Bem. 29.13 zu freien Moduln" def title_within_course - return '' unless medium.present? && medium.proper? - return local_reference if medium.teachable_type == 'Course' + return "" unless medium.present? && medium.proper? + return local_reference if medium.teachable_type == "Course" return local_reference unless medium.teachable.media_scope.term - medium.teachable.media_scope.term.to_label_short + ', ' + local_reference + "#{medium.teachable.media_scope.term.to_label_short}, #{local_reference}" end # returns the title of the item *within* a given lecture @@ -173,28 +173,28 @@ def title_within_lecture # this is true if the item belongs to a section of the lecture or the # lesson's lecture def local?(referring_medium) - return false unless section.present? + return false if section.blank? in?(referring_medium.teachable.lecture&.items.to_a) end # background color of different item sorts within thyme editor def background - return '#70db70;' if ['remark', 'theorem', 'lemma', 'corollary', - 'algorithm', 'Theorem', 'Corollary', 'Lemma', - 'proposition'].include?(sort) - return '#75d7f0;' if ['definition', 'annotation', 'example', - 'figure', 'exercise', 'equation'].include?(sort) - return 'lightgray;' if sort == 'link' || sort == 'self' + return "#70db70;" if ["remark", "theorem", "lemma", "corollary", + "algorithm", "Theorem", "Corollary", "Lemma", + "proposition"].include?(sort) + return "#75d7f0;" if ["definition", "annotation", "example", + "figure", "exercise", "equation"].include?(sort) + return "lightgray;" if sort == "link" || sort == "self" - '' + "" end # special background for sections def section_background - return 'beige;' if sort == 'section' + return "beige;" if sort == "section" - 'aliceblue;' + "aliceblue;" end # if the associated medium contains a video, returns a link to the play @@ -202,9 +202,9 @@ def section_background # result might look like this: # "/media/22/play?time=4480.5" def video_link - return if sort == 'pdf_destination' + return if sort == "pdf_destination" return unless video? - return video_link_untimed if sort == 'self' + return video_link_untimed if sort == "self" video_link_timed end @@ -226,7 +226,7 @@ def manuscript_link def quiz_link return unless quiz? - return quiz_link_generic + quiz_link_generic end # if the associated medium contains an external link, it is returned @@ -237,10 +237,10 @@ def medium_link end def self.available_sorts - ['definition', 'remark', 'lemma', 'theorem', 'example', 'annotation', - 'algorithm', 'corollary', 'section', 'label', 'subsection', 'Theorem', - 'proposition', 'Lemma', 'Corollary', 'figure', 'chapter', 'exercise', - 'equation'] + ["definition", "remark", "lemma", "theorem", "example", "annotation", + "algorithm", "corollary", "section", "label", "subsection", "Theorem", + "proposition", "Lemma", "Corollary", "figure", "chapter", "exercise", + "equation"] end def self.localized_sorts @@ -252,17 +252,17 @@ def self.inverted_sorts end def self.content_sorts - ['remark', 'theorem', 'lemma', 'definition', 'annotation', 'example', - 'algorithm', 'label', 'corollary', 'proposition', 'Lemma', 'Theorem', - 'subsection', 'Corollary', 'equation', 'exercise', 'figure', 'self'] + ["remark", "theorem", "lemma", "definition", "annotation", "example", + "algorithm", "label", "corollary", "proposition", "Lemma", "Theorem", + "subsection", "Corollary", "equation", "exercise", "figure", "self"] end def self.toc_sorts - ['chapter', 'section'] + ["chapter", "section"] end def self.external_sorts - ['link', 'pdf_destination'] + ["link", "pdf_destination"] end def self.internal_sort(sort) @@ -278,7 +278,7 @@ def manuscript? end def quiz? - medium.present? && medium.type == 'Quiz' && medium.quiz_graph.present? + medium.present? && medium.type == "Quiz" && medium.quiz_graph.present? end def medium_link? @@ -286,7 +286,7 @@ def medium_link? end def link? - sort == 'link' + sort == "link" end def referencing_media @@ -294,24 +294,24 @@ def referencing_media end def related_items_visible? - !!related_items&.first&.medium&.published? && + !related_items&.first&.medium&.published?.nil? && !related_items&.first&.medium&.locked? end private def math_items - ['remark', 'theorem', 'lemma', 'definition', 'annotation', 'example', - 'corollary', 'algorithm', 'Theorem', 'proposition', 'Lemma', 'Corollary', - 'figure', 'subsection', 'exercise', 'equation'] + ["remark", "theorem", "lemma", "definition", "annotation", "example", + "corollary", "algorithm", "Theorem", "proposition", "Lemma", "Corollary", + "figure", "subsection", "exercise", "equation"] end def other_items - ['section', 'self', 'link', 'label', 'pdf_destination', 'chapter'] + ["section", "self", "link", "label", "pdf_destination", "chapter"] end def proper_link? - sort == 'link' && link.present? + sort == "link" && link.present? end def next_item @@ -331,25 +331,25 @@ def math_item_number end def math_reference - sort_long + ' ' + math_item_number + "#{sort_long} #{math_item_number}" end def special_reference - return 'Medium' if sort == 'self' - return '' if sort == 'pdf_destination' + return "Medium" if sort == "self" + return "" if sort == "pdf_destination" - 'extern' + "extern" end def section_reference return section.displayed_number.to_s if section.present? - return '§' + ref_number if ref_number.present? + return "§#{ref_number}" if ref_number.present? - '' + "" end def chapter_reference - chapter_short = I18n.t('admin.item.chapter_short', + chapter_short = I18n.t("admin.item.chapter_short", locale: locale) return "#{chapter_short} #{ref_number}" if ref_number.present? @@ -357,52 +357,46 @@ def chapter_reference end def toc_reference - return section_reference if sort == 'section' - return chapter_reference if sort == 'chapter' + return section_reference if sort == "section" + return chapter_reference if sort == "chapter" - if sort == 'label' - return '' if description.present? + if sort == "label" + return "" if description.present? - return 'destination: ' + pdf_destination.to_s + return "destination: #{pdf_destination}" end special_reference end def non_math_reference - return medium.title_for_viewers if sort == 'self' - if sort == 'pdf_destination' - return medium.title_for_viewers + ' (pdf) # ' + description - end + return medium.title_for_viewers if sort == "self" + return "#{medium.title_for_viewers} (pdf) # #{description}" if sort == "pdf_destination" - 'extern ' + description.to_s if sort == 'link' + "extern #{description}" if sort == "link" end def local_non_math_reference - return medium.local_title_for_viewers if sort == 'self' - if sort == 'pdf_destination' - return medium.local_title_for_viewers + ' (pdf) # ' + description - end + return medium.local_title_for_viewers if sort == "self" + return "#{medium.local_title_for_viewers} (pdf) # #{description}" if sort == "pdf_destination" - 'extern ' + description.to_s if sort == 'link' + "extern #{description}" if sort == "link" end def short_ref_with_teachable - unless short_reference.present? - return medium.teachable.lecture.title_for_viewers - end + return medium.teachable.lecture.title_for_viewers if short_reference.blank? - medium.teachable.lecture.title_for_viewers + ', ' + short_reference + "#{medium.teachable.lecture.title_for_viewers}, #{short_reference}" end def short_ref_with_description - return short_reference + ' ' + description.to_s unless sort == 'section' + return "#{short_reference} #{description}" unless sort == "section" short_ref_for_sections end def short_ref_for_sections - return short_reference + ' ' + description if description.present? - return short_reference + ' ' + section.title if section.present? + return "#{short_reference} #{description}" if description.present? + return "#{short_reference} #{section.title}" if section.present? short_reference end @@ -447,13 +441,13 @@ def valid_start_time end def start_time_not_required - medium.nil? || medium.sort == 'Script' || sort == 'self' || - sort == 'pdf_destination' || !start_time&.valid? || !medium.video + medium.nil? || medium.sort == "Script" || sort == "self" || + sort == "pdf_destination" || !start_time&.valid? || !medium.video end def start_time_not_too_late return true if start_time_not_required - return true if start_time.total_seconds <= medium.video.metadata['duration'] + return true if start_time.total_seconds <= medium.video.metadata["duration"] errors.add(:start_time, :too_late) false @@ -477,7 +471,7 @@ def no_duplicate_start_time end def nonempty_link_or_explanation - return true if sort != 'link' + return true if sort != "link" return true if link.present? return true if explanation.present? diff --git a/app/models/item_self_join.rb b/app/models/item_self_join.rb index 8f03219d2..d3ca149e4 100644 --- a/app/models/item_self_join.rb +++ b/app/models/item_self_join.rb @@ -5,14 +5,16 @@ # in a lesson medium) class ItemSelfJoin < ApplicationRecord belongs_to :item - belongs_to :related_item, class_name: 'Item' + belongs_to :related_item, class_name: "Item" + # rubocop:todo Rails/UniqueValidationWithoutIndex validates :related_item, uniqueness: { scope: :item } + before_destroy :touch_item + after_destroy :destroy_inverses, if: :inverse? + # rubocop:enable Rails/UniqueValidationWithoutIndex after_save :create_inverse, unless: :inverse? after_save :destroy, if: :self_inverse? after_save :touch_item - before_destroy :touch_item - after_destroy :destroy_inverses, if: :inverse? private diff --git a/app/models/lecture.rb b/app/models/lecture.rb index a81bda896..8d5c1d353 100644 --- a/app/models/lecture.rb +++ b/app/models/lecture.rb @@ -5,26 +5,33 @@ class Lecture < ApplicationRecord belongs_to :course # teacher is the user that gives the lecture - belongs_to :teacher, class_name: 'User', foreign_key: 'teacher_id' + belongs_to :teacher, class_name: "User" # a lecture takes place in a certain term, except those where the course # is marked as term_independent belongs_to :term, optional: true # a lecture has many chapters, who have positions - has_many :chapters, -> { order(position: :asc) }, dependent: :destroy + has_many :chapters, -> { order(position: :asc) }, + dependent: :destroy, + inverse_of: :lecture # during the term, a lot of lessons take place for this lecture has_many :lessons, -> { order(date: :asc, id: :asc) }, dependent: :destroy, after_add: :touch_siblings, - after_remove: :touch_siblings + after_remove: :touch_siblings, + inverse_of: :lecture # a lecture has many talks, which have positions - has_many :talks, -> { order(position: :asc) }, dependent: :destroy + has_many :talks, -> { order(position: :asc) }, + dependent: :destroy, + inverse_of: :lecture # being a teachable (course/lecture/lesson), a lecture has associated media - has_many :media, -> { order(position: :asc) }, as: :teachable + has_many :media, -> { order(position: :asc) }, + as: :teachable, + inverse_of: :teachable # in a lecture, you can import other media has_many :imports, as: :teachable, dependent: :destroy @@ -50,7 +57,8 @@ class Lecture < ApplicationRecord has_many :announcements, dependent: :destroy # a lecture has many tutorials - has_many :tutorials, -> { order(:title) } + has_many :tutorials, -> { order(:title) }, + inverse_of: :lecture # a lecture has many assignments (e.g. exercises with deadlines) has_many :assignments @@ -61,14 +69,16 @@ class Lecture < ApplicationRecord # we do not allow that a teacher gives a certain lecture in a given term # of the same sort twice + # rubocop:todo Rails/UniqueValidationWithoutIndex validates :course, uniqueness: { scope: [:teacher_id, :term_id, :sort] } + # rubocop:enable Rails/UniqueValidationWithoutIndex - validates :content_mode, inclusion: { in: ['video', 'manuscript'] } + validates :content_mode, inclusion: { in: ["video", "manuscript"] } - validates :sort, inclusion: { in: ['lecture', 'seminar', 'oberseminar', - 'proseminar', 'special'] } + validates :sort, inclusion: { in: ["lecture", "seminar", "oberseminar", + "proseminar", "special"] } - validates_presence_of :term, unless: :term_independent? + validates :term, presence: { unless: :term_independent? } validate :absence_of_term, if: :term_independent? @@ -84,6 +94,9 @@ class Lecture < ApplicationRecord greater_than: -1 }, allow_nil: true + # if the lecture is destroyed, its forum (if existent) should be destroyed + # as well + before_destroy :destroy_forum # as a teacher has editing rights by definition, we do not need him in the # list of editors after_save :remove_teacher_as_editor @@ -95,18 +108,14 @@ class Lecture < ApplicationRecord after_save :touch_chapters after_save :touch_sections - # if the lecture is destroyed, its forum (if existent) should be destroyed - # as well - before_destroy :destroy_forum - # scopes scope :published, -> { where.not(released: nil) } scope :no_term, -> { where(term: nil) } - scope :restricted, -> { where.not(passphrase: ['', nil]) } + scope :restricted, -> { where.not(passphrase: ["", nil]) } - scope :seminar, -> { where(sort: ['seminar', 'oberseminar', 'proseminar']) } + scope :seminar, -> { where(sort: ["seminar", "oberseminar", "proseminar"]) } searchable do integer :term_id do @@ -155,7 +164,7 @@ def media_scope end def selector_value - 'Lecture-' + id.to_s + "Lecture-#{id}" end def title @@ -209,7 +218,7 @@ def card_header_path(user) end def cache_key - super + '-' + I18n.locale.to_s + "#{super}-#{I18n.locale}" end def restricted? @@ -220,7 +229,7 @@ def visible_for_user?(user) return true if user.admin return true if edited_by?(user) return false unless published? - return false if restricted? && !self.in?(user.lectures) + return false if restricted? && !in?(user.lectures) true end @@ -260,10 +269,10 @@ def tags_including_media_tags (tags + lessons.includes(media: :tags) .map(&:media).flatten.uniq - .select { |m| m.released.in?(['all', 'users', 'subscribers']) } + .select { |m| m.released.in?(["all", "users", "subscribers"]) } .map(&:tags).flatten + media.includes(:tags) - .select { |m| m.released.in?(['all', 'users', 'subscribers']) } + .select { |m| m.released.in?(["all", "users", "subscribers"]) } .map(&:tags).flatten).uniq end @@ -285,7 +294,7 @@ def script_items_by_position hidden_sections = Section.where(hidden: true) .or(Section.where(chapter: hidden_chapters)) Item.where(medium: lecture.manuscript) - .where.not(sort: 'self') + .where.not(sort: "self") .content .unquarantined .unhidden @@ -294,22 +303,22 @@ def script_items_by_position end def manuscript - Medium.where(sort: 'Script', teachable: lecture)&.first + Medium.where(sort: "Script", teachable: lecture)&.first end # returns the ARel of all media whose teachable's lecture is the given lecture def media_with_inheritance_uncached Medium.proper.where(teachable: self) - .or(Medium.proper.where(teachable: self.lessons)) - .or(Medium.proper.where(teachable: self.talks)) + .or(Medium.proper.where(teachable: lessons)) + .or(Medium.proper.where(teachable: talks)) end def media_with_inheritance_uncached_eagerload_stuff Medium.includes(:tags, teachable: [lecture: [:lessons, :talks]]) .proper.where(teachable: self) .or(Medium.includes(:tags, teachable: [lecture: [:lessons, :talks]]) - .proper.where(teachable: self.lessons + self.talks)) + .proper.where(teachable: lessons + talks)) end def media_with_inheritance @@ -336,35 +345,35 @@ def published? # These methods make use of caching. def kaviar?(user) - project?('kaviar', user) || imported_any?('kaviar') + project?("kaviar", user) || imported_any?("kaviar") end def sesam?(user) - project?('sesam', user) || imported_any?('sesam') + project?("sesam", user) || imported_any?("sesam") end def keks?(user) - project?('keks', user) || imported_any?('keks') + project?("keks", user) || imported_any?("keks") end def erdbeere?(user) - project?('erdbeere', user) || imported_any?('erdbeere') + project?("erdbeere", user) || imported_any?("erdbeere") end def kiwi?(user) - project?('kiwi', user) || imported_any?('kiwi') + project?("kiwi", user) || imported_any?("kiwi") end def nuesse?(user) - project?('nuesse', user) || imported_any?('nuesse') + project?("nuesse", user) || imported_any?("nuesse") end def script?(user) - project?('script', user) || imported_any?('nuesse') + project?("script", user) || imported_any?("nuesse") end def reste?(user) - project?('reste', user) || imported_any?('reste') + project?("reste", user) || imported_any?("reste") end # the next methods put together some information on the lecture (teacher, @@ -379,7 +388,7 @@ def short_title def short_title_release return short_title if published? - "#{short_title} (#{I18n.t('access.unpublished')})" + "#{short_title} (#{I18n.t("access.unpublished")})" end def short_title_brackets @@ -401,8 +410,8 @@ def title_with_teacher_no_type end def term_teacher_info - return term_to_label unless teacher.present? - return term_to_label unless teacher.name.present? + return term_to_label if teacher.blank? + return term_to_label if teacher.name.blank? return "#{course.title}, #{teacher.name}" unless term "(#{sort_localized_short}) #{term_to_label}, #{teacher.name}" @@ -411,7 +420,7 @@ def term_teacher_info def term_teacher_published_info return term_teacher_info if published? - "#{term_teacher_info} (#{I18n.t('access.unpublished')})" + "#{term_teacher_info} (#{I18n.t("access.unpublished")})" end def title_term_info @@ -496,11 +505,11 @@ def select_editors def self.editable_selection(user) if user.admin? return Lecture.sort_by_date(Lecture.includes(:term).all) - .map { |l| [l.title_for_viewers, 'Lecture-' + l.id.to_s] } + .map { |l| [l.title_for_viewers, "Lecture-#{l.id}"] } end Lecture.sort_by_date(Lecture.includes(:course, :editors).all) .select { |l| l.edited_by?(user) } - .map { |l| [l.title_for_viewers, 'Lecture-' + l.id.to_s] } + .map { |l| [l.title_for_viewers, "Lecture-#{l.id}"] } end # the next methods provide infos on editors and teacher @@ -561,7 +570,7 @@ def lecture_lesson_results(filtered_media) end def order_factor - return -1 unless lecture.term.present? + return -1 if lecture.term.blank? return -1 if lecture.term.active 1 @@ -569,7 +578,7 @@ def order_factor def begin_date Rails.cache.fetch("#{cache_key_with_version}/begin_date") do - term&.begin_date || Term.active.begin_date || Date.today + term&.begin_date || Term.active.begin_date || Time.zone.today end end @@ -588,7 +597,7 @@ def forum? end def forum - Thredded::Messageboard.find_by_id(forum_id) + Thredded::Messageboard.find_by(id: forum_id) end # extract how many posts in the lecture's forum have not been read @@ -612,11 +621,11 @@ def lecture_path end def self.sorts - ['lecture', 'seminar', 'proseminar', 'oberseminar'] + ["lecture", "seminar", "proseminar", "oberseminar"] end def self.sort_localized - Lecture.sorts.map { |s| [s, I18n.t("admin.lecture.#{s}")] }.to_h + Lecture.sorts.index_with { |s| I18n.t("admin.lecture.#{s}") } end def self.select_sorts @@ -624,19 +633,19 @@ def self.select_sorts end def seminar? - return true if sort.in?(['seminar', 'proseminar', 'oberseminar']) + return true if sort.in?(["seminar", "proseminar", "oberseminar"]) false end def chapter_name - return 'chapter' unless seminar? + return "chapter" unless seminar? - 'talk' + "talk" end def comments_closed? - media_with_inheritance.map(&:commontator_thread).map(&:is_closed?).all? + media_with_inheritance.map { |media| media.commontator_thread.is_closed? }.all? end def close_comments!(user) @@ -645,7 +654,7 @@ def close_comments!(user) end end - def open_comments!(user) + def open_comments!(_user) media_with_inheritance.select { |m| m.commontator_thread.is_closed? } .each { |m| m.commontator_thread.reopen } end @@ -656,12 +665,12 @@ def self.in_current_term def <=>(other) return 0 if self == other - return 1 if self.begin_date < other.begin_date - return 1 if self.term == other.term && - ActiveSupport::Inflector.transliterate(self.course.title) > + return 1 if begin_date < other.begin_date + return 1 if term == other.term && + ActiveSupport::Inflector.transliterate(course.title) > ActiveSupport::Inflector.transliterate(other.course.title) - return 1 if self.term == other.term && self.course == other.course && - self.sort_localized < other.sort_localized + return 1 if term == other.term && course == other.course && + sort_localized < other.sort_localized -1 end @@ -671,29 +680,43 @@ def subscribed_by?(user) end def self.search_by(search_params, page) - search_params[:types] = - [] if search_params[:all_types] == '1' || search_params[:types].nil? - search_params[:term_ids] = - [] if search_params[:all_terms] == '1' || search_params[:term_ids].nil? - search_params[:teacher_ids] = - [] if search_params[:all_teachers] == '1' || search_params[:teacher_ids].nil? - search_params[:program_ids] = - [] if search_params[:all_programs] == '1' || search_params[:program_ids].nil? + if search_params[:all_types] == "1" || search_params[:types].nil? + search_params[:types] = + [] + end + if search_params[:all_terms] == "1" || search_params[:term_ids].nil? + search_params[:term_ids] = + [] + end + if search_params[:all_teachers] == "1" || search_params[:teacher_ids].nil? + search_params[:teacher_ids] = + [] + end + if search_params[:all_programs] == "1" || search_params[:program_ids].nil? + search_params[:program_ids] = + [] + end search = Sunspot.new_search(Lecture) # add lectures without term to current term if Term.active.try(:id).to_i.to_s.in?(search_params[:term_ids]) - search_params[:term_ids].push('0') + search_params[:term_ids].push("0") end search.build do with(:sort, search_params[:types]) unless search_params[:types].empty? - with(:teacher_id, - search_params[:teacher_ids]) unless search_params[:teacher_ids].empty? - with(:program_ids, - search_params[:program_ids]) unless search_params[:program_ids].empty? - with(:term_id, - search_params[:term_ids]) unless search_params[:term_ids].empty? - end - admin = User.find_by_id(search_params[:user_id])&.admin + unless search_params[:teacher_ids].empty? + with(:teacher_id, + search_params[:teacher_ids]) + end + unless search_params[:program_ids].empty? + with(:program_ids, + search_params[:program_ids]) + end + unless search_params[:term_ids].empty? + with(:term_id, + search_params[:term_ids]) + end + end + admin = User.find_by(id: search_params[:user_id])&.admin unless admin search.build do any_of do @@ -705,13 +728,13 @@ def self.search_by(search_params, page) end if search_params[:fulltext].present? search.build do - fulltext search_params[:fulltext] + fulltext(search_params[:fulltext]) end end search.build do order_by(:sort_date, :desc) order_by(:sort_title, :asc) - paginate page: page, per_page: search_params[:per] + paginate(page: page, per_page: search_params[:per]) end search end @@ -719,13 +742,13 @@ def self.search_by(search_params, page) def term_to_label return term.to_label if term - '' + "" end def term_to_label_short return term.to_label_short if term - '' + "" end def tutors @@ -735,7 +758,7 @@ def tutors def submission_deletion_date Rails.cache.fetch("#{cache_key_with_version}/submission_deletion_date") do - (term&.end_date || Term.active&.end_date || (Date.today + 180.days)) + + (term&.end_date || Term.active&.end_date || (Time.zone.today + 180.days)) + 15.days end end @@ -745,21 +768,21 @@ def assignments_by_deadline end def current_assignments - assignments_by_deadline.select { |x| x.first >= Time.now }.first&.second + assignments_by_deadline.find { |x| x.first >= Time.zone.now }&.second .to_a end def previous_assignments - assignments_by_deadline.select { |x| x.first < Time.now }.last&.second.to_a + assignments_by_deadline.reverse.find { |x| x.first < Time.zone.now }&.second.to_a end def scheduled_assignments? - media.where(sort: 'Nuesse').where.not(publisher: nil) + media.where(sort: "Nuesse").where.not(publisher: nil) .any? { |m| m.publisher.create_assignment } end def scheduled_assignments - media.where(sort: 'Nuesse').where.not(publisher: nil) + media.where(sort: "Nuesse").where.not(publisher: nil) .select { |m| m.publisher.create_assignment } .map { |m| m.publisher.assignment } end @@ -803,11 +826,13 @@ def import_toc!(imported_lecture, import_sections, import_tags) def speakers return User.none unless seminar? - User.where(id: SpeakerTalkJoin.where(talk: talks).pluck(:speaker_id)) + + User.where(id: SpeakerTalkJoin.where(talk: talks).select(:speaker_id)) end def older_than?(timespan) return true unless term + term.begin_date <= Term.active.begin_date - timespan end @@ -826,25 +851,25 @@ def remove_teacher_as_editor # to this lecture and a given project (kaviar, sesam etc.) def project_as_user?(project) Rails.cache.fetch("#{cache_key_with_version}/#{project}") do - Medium.where(sort: medium_sort[project], - released: ['all', 'users', 'subscribers'], - teachable: self).exists? || - Medium.where(sort: medium_sort[project], - released: ['all', 'users', 'subscribers'], - teachable: lessons).exists? || - Medium.where(sort: medium_sort[project], - released: ['all', 'users', 'subscribers'], - teachable: talks).exists? || - Medium.where(sort: medium_sort[project], - released: ['all', 'users', 'subscribers'], - teachable: course).exists? + Medium.exists?(sort: medium_sort[project], + released: ["all", "users", "subscribers"], + teachable: self) || + Medium.exists?(sort: medium_sort[project], + released: ["all", "users", "subscribers"], + teachable: lessons) || + Medium.exists?(sort: medium_sort[project], + released: ["all", "users", "subscribers"], + teachable: talks) || + Medium.exists?(sort: medium_sort[project], + released: ["all", "users", "subscribers"], + teachable: course) end end def imported_any?(project) Rails.cache.fetch("#{cache_key_with_version}/imported_#{project}") do imported_media.exists?(sort: medium_sort[project], - released: ['all', 'users']) + released: ["all", "users"]) end end @@ -852,47 +877,47 @@ def project?(project, user) return project_as_user?(project) unless edited_by?(user) || user.admin course_media = if user.in?(course.editors) || user.admin - Medium.where(sort: medium_sort[project], - teachable: course).exists? + Medium.exists?(sort: medium_sort[project], + teachable: course) else - Medium.where(sort: medium_sort[project], - released: ['all', 'users', 'subscribers'], - teachable: course).exists? + Medium.exists?(sort: medium_sort[project], + released: ["all", "users", "subscribers"], + teachable: course) end - lecture_media = Medium.where(sort: medium_sort[project], - teachable: self).exists? - lesson_media = Medium.where(sort: medium_sort[project], - teachable: lessons).exists? - talk_media = Medium.where(sort: medium_sort[project], - teachable: talks).exists? + lecture_media = Medium.exists?(sort: medium_sort[project], + teachable: self) + lesson_media = Medium.exists?(sort: medium_sort[project], + teachable: lessons) + talk_media = Medium.exists?(sort: medium_sort[project], + teachable: talks) course_media || lecture_media || lesson_media || talk_media end def medium_sort - { 'kaviar' => ['Kaviar'], 'sesam' => ['Sesam'], 'kiwi' => ['Kiwi'], - 'keks' => ['Quiz'], 'nuesse' => ['Nuesse'], - 'erdbeere' => ['Erdbeere'], 'script' => ['Script'], 'reste' => ['Reste'] } + { "kaviar" => ["Kaviar"], "sesam" => ["Sesam"], "kiwi" => ["Kiwi"], + "keks" => ["Quiz"], "nuesse" => ["Nuesse"], + "erdbeere" => ["Erdbeere"], "script" => ["Script"], "reste" => ["Reste"] } end def touch_media - media_with_inheritance.update_all(updated_at: Time.now) + media_with_inheritance.touch_all end def touch_lessons - lessons.update_all(updated_at: Time.now) + lessons.touch_all end - def touch_siblings(lesson) - lessons.update_all(updated_at: Time.now) - Medium.where(teachable: lessons).update_all(updated_at: Time.now) + def touch_siblings(_lesson) + lessons.touch_all + Medium.where(teachable: lessons).touch_all end def touch_chapters - chapters.update_all(updated_at: Time.now) + chapters.touch_all end def touch_sections - Section.where(chapter: chapters).update_all(updated_at: Time.now) + Section.where(chapter: chapters).touch_all end def destroy_forum diff --git a/app/models/lesson.rb b/app/models/lesson.rb index 852e1dfff..e2dad90be 100644 --- a/app/models/lesson.rb +++ b/app/models/lesson.rb @@ -16,11 +16,15 @@ class Lesson < ApplicationRecord # being a teachable (course/lecture/lesson), a lesson has associated media has_many :media, -> { order(position: :asc) }, - as: :teachable + as: :teachable, + inverse_of: :teachable validates :date, presence: true validates :sections, presence: true + before_destroy :touch_media + before_destroy :touch_siblings + before_destroy :touch_sections, prepend: true # media are cached in several places # media are touched in order to find out whether cache is out of date after_save :touch_media @@ -29,9 +33,6 @@ class Lesson < ApplicationRecord after_save :touch_siblings after_save :touch_self after_save :touch_tags - before_destroy :touch_media - before_destroy :touch_siblings - before_destroy :touch_sections, prepend: true delegate :editors_with_inheritance, to: :lecture, allow_nil: true @@ -39,7 +40,7 @@ class Lesson < ApplicationRecord # Therefore, they can be called on any *teachable* def course - return unless lecture.present? + return if lecture.blank? lecture.course end @@ -57,46 +58,45 @@ def media_scope end def selector_value - 'Lesson-' + id.to_s + "Lesson-#{id}" end def title - I18n.t('lesson') + ' ' + number.to_s + ', ' + date_localized.to_s + "#{I18n.t("lesson")} #{number}, #{date_localized}" end def to_label - 'Nr. ' + number.to_s + ', ' + date_localized.to_s + "Nr. #{number}, #{date_localized}" end def compact_title - lecture.compact_title + '.E' + number.to_s + "#{lecture.compact_title}.E#{number}" end def cache_key - super + '-' + I18n.locale.to_s + "#{super}-#{I18n.locale}" end def title_for_viewers Rails.cache.fetch("#{cache_key_with_version}/title_for_viewers") do - lecture.title_for_viewers + ', ' + I18n.t('lesson') + ' ' + number.to_s + - ' ' + I18n.t('from') + ' ' + date_localized + lesson_str = "#{I18n.t("lesson")} #{number}" + date_str = "#{I18n.t("from")} #{date_localized}" + "#{lecture.title_for_viewers}, #{lesson_str} #{date_str}" end end def long_title - lecture.title + ', ' + title + "#{lecture.title}, #{title}" end - def locale_with_inheritance - lecture.locale_with_inheritance - end + delegate :locale_with_inheritance, to: :lecture def locale locale_with_inheritance end def card_header - lecture.short_title_brackets + ', ' + date_localized + "#{lecture.short_title_brackets}, #{date_localized}" end def card_header_path(user) @@ -105,36 +105,30 @@ def card_header_path(user) lesson_path end - def published? - lecture.published? - end + delegate :published?, to: :lecture # some more methods dealing with the title def short_title_with_lecture - lecture.short_title + ', S.' + number.to_s + "#{lecture.short_title}, S.#{number}" end def short_title_with_lecture_date - lecture.short_title + ', ' + date_localized + "#{lecture.short_title}, #{date_localized}" end def short_title - lecture.short_title + '_E' + number.to_s + "#{lecture.short_title}_E#{number}" end def local_title_for_viewers - "#{I18n.t('lesson')} #{number} #{I18n.t('from')} #{date_localized}" - end - - def restricted? - lecture.restricted? + "#{I18n.t("lesson")} #{number} #{I18n.t("from")} #{date_localized}" end # more infos that can be extracted def term - return unless lecture.present? + return if lecture.blank? lecture.term end @@ -158,9 +152,7 @@ def visible_media_for_user(user) media.select { |m| m.visible_for_user?(user) } end - def visible_for_user?(user) - lecture.visible_for_user?(user) - end + delegate :visible_for_user?, to: :lecture # the number of a lesson is calculated by its date relative to the other # lessons @@ -169,17 +161,15 @@ def number end def date_localized - I18n.localize date, format: :concise + I18n.l(date, format: :concise) end def section_titles - sections.map(&:title).join(', ') + sections.map(&:title).join(", ") end # a lesson can be edited by any user who can edit its lecture - def edited_by?(user) - lecture.edited_by?(user) - end + delegate :edited_by?, to: :lecture def section_tags sections.collect(&:tags).flatten @@ -201,13 +191,13 @@ def visible_items end def content_items - return visible_items if lecture.content_mode == 'video' + return visible_items if lecture.content_mode == "video" script_items end def content - ([details] + media.potentially_visible.map(&:content)).compact - [''] + ([details] + media.potentially_visible.map(&:content)).compact - [""] end def singular_medium @@ -228,7 +218,7 @@ def script_items return [] unless start_item && end_item range = (start_item.position..end_item.position).to_a - return [] unless range.present? + return [] if range.blank? hidden_chapters = Chapter.where(hidden: true) hidden_sections = Section.where(hidden: true) @@ -258,12 +248,12 @@ def self.editable_selection(user) if user.admin? return Lesson.order_reverse .map do |l| - [l.title_for_viewers, 'Lesson-' + l.id.to_s] + [l.title_for_viewers, "Lesson-#{l.id}"] end end Lesson.includes(:lecture).order_reverse .select { |l| l.edited_by?(user) } - .map { |l| [l.title_for_viewers, 'Lesson-' + l.id.to_s] } + .map { |l| [l.title_for_viewers, "Lesson-#{l.id}"] } end def guess_start_destination @@ -288,7 +278,7 @@ def probable_start_destination position = end_item.position return unless position - successor = lecture.script_items_by_position.where('position > ?', position) + successor = lecture.script_items_by_position.where("position > ?", position) .order(:position)&.first&.pdf_destination return successor if successor @@ -308,16 +298,16 @@ def lesson_path # used for after save callback def touch_media - lecture.media_with_inheritance.update_all(updated_at: Time.now) + lecture.media_with_inheritance.touch_all end def touch_siblings - lecture.lessons.update_all(updated_at: Time.now) + lecture.lessons.touch_all end def touch_sections - sections.update_all(updated_at: Time.now) - chapters = sections.map(&:chapter) + sections.touch_all + sections.map(&:chapter) sections.map(&:chapter).each(&:touch) lecture.touch end @@ -327,7 +317,7 @@ def touch_self end def touch_tags - tags.update_all(updated_at: Time.now) + tags.touch_all end def touch_section(section) diff --git a/app/models/link.rb b/app/models/link.rb index 285eef6c0..2591caa8d 100644 --- a/app/models/link.rb +++ b/app/models/link.rb @@ -3,17 +3,17 @@ # describes which media are related to a given medium class Link < ApplicationRecord belongs_to :medium - belongs_to :linked_medium, class_name: 'Medium' + belongs_to :linked_medium, class_name: "Medium" # we do not want duplicate entries validates :linked_medium, uniqueness: { scope: :medium } + # after a link is destroyed, destroy the link in the other direction as well + after_destroy :destroy_inverses, if: :inverse? # after saving, we symmetrize the relation after_save :create_inverse, unless: :inverse? # we do not want a medium to be in relation to itself after_save :destroy, if: :self_inverse? - # after a link is destroyed, destroy the link in the other direction as well - after_destroy :destroy_inverses, if: :inverse? private diff --git a/app/models/mampf_expression.rb b/app/models/mampf_expression.rb index b0c09afc1..06fb38bb5 100644 --- a/app/models/mampf_expression.rb +++ b/app/models/mampf_expression.rb @@ -10,10 +10,10 @@ def initialize(value, tex, nerd) end def self.trivial_instance - MampfExpression.new('0', '0', '0') + MampfExpression.new("0", "0", "0") end def self.from_hash(content) - MampfExpression.new(content['0'], content['tex'], content['nerd']) + MampfExpression.new(content["0"], content["tex"], content["nerd"]) end end diff --git a/app/models/mampf_matrix.rb b/app/models/mampf_matrix.rb index fbe5c1562..65f4ad269 100644 --- a/app/models/mampf_matrix.rb +++ b/app/models/mampf_matrix.rb @@ -12,25 +12,23 @@ def initialize(row_count, column_count, coefficients, tex, nerd) end def self.trivial_instance - self.new(2, 2, - ['0', '0', '0', '0'], - '\begin{pmatrix} 0 & 0 \cr 0 & 0 \end{pmatrix}', - 'matrix([0,0],[0,0]') + new(2, 2, + ["0", "0", "0", "0"], + '\begin{pmatrix} 0 & 0 \cr 0 & 0 \end{pmatrix}', + "matrix([0,0],[0,0]") end - def entry(i, j) - if i > @row_count || j > @column_count - return '0' - end + def entry(i, j) # rubocop:disable Naming/MethodParameterName + return "0" if i > @row_count || j > @column_count - @coefficients[(i - 1) * @column_count + (j - 1)] + @coefficients[((i - 1) * @column_count) + (j - 1)] end def self.from_hash(content) - row_count = content['row_count'].to_i - column_count = content['column_count'].to_i - tex = content['tex'] - nerd = content['nerd'] + row_count = content["row_count"].to_i + column_count = content["column_count"].to_i + tex = content["tex"] + nerd = content["nerd"] coefficients = [] (1..row_count).each do |i| (1..column_count).each do |j| diff --git a/app/models/mampf_set.rb b/app/models/mampf_set.rb index 333e8d08a..3b00fff2d 100644 --- a/app/models/mampf_set.rb +++ b/app/models/mampf_set.rb @@ -10,10 +10,10 @@ def initialize(value, tex, nerd) end def self.trivial_instance - self.new('0,1', '\{0,1\}', 'vector(0,1)') + new("0,1", '\{0,1\}', "vector(0,1)") end def self.from_hash(content) - MampfSet.new(content['0'], content['tex'], content['nerd']) + MampfSet.new(content["0"], content["tex"], content["nerd"]) end end diff --git a/app/models/mampf_tuple.rb b/app/models/mampf_tuple.rb index dd997f92a..ac61b8016 100644 --- a/app/models/mampf_tuple.rb +++ b/app/models/mampf_tuple.rb @@ -10,10 +10,10 @@ def initialize(value, tex, nerd) end def self.trivial_instance - self.new('0,1', '(0,1)', 'vector(0,1)') + new("0,1", "(0,1)", "vector(0,1)") end def self.from_hash(content) - MampfTuple.new(content['0'], content['tex'], content['nerd']) + MampfTuple.new(content["0"], content["tex"], content["nerd"]) end end diff --git a/app/models/manuscript.rb b/app/models/manuscript.rb index f11e245e7..3316d5fb7 100644 --- a/app/models/manuscript.rb +++ b/app/models/manuscript.rb @@ -8,8 +8,8 @@ class Manuscript :content_descriptions, :version def initialize(medium) - unless medium && medium.sort == 'Script' && - medium&.teachable_type == 'Lecture' && + unless medium && medium.sort == "Script" && + medium&.teachable_type == "Lecture" && medium.manuscript return end @@ -17,17 +17,17 @@ def initialize(medium) @medium = medium @lecture = medium.teachable.lecture @locale = @lecture.locale_with_inheritance || I18n.default_locale - @chapter_marker = I18n.t('manuscript.chapter', locale: @locale) - @section_marker = I18n.t('manuscript.section', locale: @locale) - @version = medium.manuscript.metadata['version'] - bookmarks = medium.manuscript.metadata['bookmarks'] || [] + @chapter_marker = I18n.t("manuscript.chapter", locale: @locale) + @section_marker = I18n.t("manuscript.section", locale: @locale) + @version = medium.manuscript.metadata["version"] + bookmarks = medium.manuscript.metadata["bookmarks"] || [] @chapters = get_chapters(bookmarks) match_mampf_chapters @sections = get_sections(bookmarks) match_mampf_sections @content = get_content(bookmarks) check_content - @content_descriptions = @content.map { |c| c['description'] } - [''] + @content_descriptions = @content.pluck("description") - [""] add_info_on_tag_ids add_info_on_item_ids_and_hidden_status @contradictions = determine_contradictions @@ -40,49 +40,49 @@ def empty? end def sections_in_chapter(chapter) - @sections.select { |s| s['chapter'] == chapter['chapter'] } - .sort_by { |s| s['counter'] } + @sections.select { |s| s["chapter"] == chapter["chapter"] } + .sort_by { |s| s["counter"] } end def content_in_section(section) - @content.select { |c| c['section'] == section['section'] } - .sort_by { |c| c['counter'] } + @content.select { |c| c["section"] == section["section"] } + .sort_by { |c| c["counter"] } end # returns those content bookmarks who have a chapter or section counter # that corresponds to a chapter or section without a bookmark def content_in_unbookmarked_locations - @content.select { |c| c['contradiction'] } + @content.select { |c| c["contradiction"] } end def content_in_unbookmarked_locations? - @content.any? { |c| c['contradiction'] } + @content.any? { |c| c["contradiction"] } end def sections_in_unbookmarked_chapters - @sections.select { |s| s['contradiction'] == :missing_chapter } + @sections.select { |s| s["contradiction"] == :missing_chapter } end def sections_in_unbookmarked_chapters? - @sections.any? { |s| s['contradiction'] == :missing_chapter } + @sections.any? { |s| s["contradiction"] == :missing_chapter } end # returns the matching chapter in mampf for the given manuscript chapter # (matching is done by label) def chapter_in_mampf(chapter) @lecture&.chapters - &.find { |chap| chap.reference == chapter['label'] } + &.find { |chap| chap.reference == chapter["label"] } end def section_in_mampf(section) @lecture&.sections_cached - &.find { |sec| sec.reference == section['label'] } + &.find { |sec| sec.reference == section["label"] } end # returns if the mampf chapter for the corresponding chapter has a different # title def manuscript_chapter_contradicts?(chapter) - chapter_in_mampf(chapter)&.title != chapter['description'] + chapter_in_mampf(chapter)&.title != chapter["description"] end # export manuscript to database: @@ -95,7 +95,7 @@ def export_to_db!(filter_boxes) create_new_chapters! @chapters.each do |c| create_new_sections!(c) - c['mampf_chapter'] = c['mampf_chapter'].reload + c["mampf_chapter"] = c["mampf_chapter"].reload end create_or_update_chapter_items! create_or_update_section_items! @@ -105,12 +105,12 @@ def export_to_db!(filter_boxes) # chapters in mampf that are not represented in the manuscript def unmatched_mampf_chapters - chapters_in_mampf = @chapters.map { |c| c['mampf_chapter'] }.compact + chapters_in_mampf = @chapters.filter_map { |c| c["mampf_chapter"] } @lecture.chapters - chapters_in_mampf end def unmatched_mampf_sections - sections_in_mampf = @sections.map { |s| s['mampf_section'] }.compact + sections_in_mampf = @sections.filter_map { |s| s["mampf_section"] } @lecture.sections - sections_in_mampf end @@ -119,45 +119,45 @@ def create_new_chapters! new_chapters.each do |c| chap = Chapter.new(lecture_id: @lecture.id, title: c.second) chap.insert_at(c.first) - corresponding = @chapters.find { |d| d['counter'] == c.third } - corresponding['mampf_chapter'] = chap + corresponding = @chapters.find { |d| d["counter"] == c.third } + corresponding["mampf_chapter"] = chap end @lecture = @lecture.reload end # create sections in mampf for those manuscript sections not yet in mampf def create_new_sections!(chapter) - return if chapter['mampf_chapter'].nil? + return if chapter["mampf_chapter"].nil? - mampf_chapter = chapter['mampf_chapter'] + mampf_chapter = chapter["mampf_chapter"] new_sections_in_chapter(chapter).each do |s| sect = Section.new(chapter_id: mampf_chapter.id, title: s.second) sect.insert_at(s.first) - corresponding = @sections.find { |d| d['counter'] == s.third } - corresponding['mampf_section'] = sect + corresponding = @sections.find { |d| d["counter"] == s.third } + corresponding["mampf_section"] = sect end end def create_or_update_chapter_items! - destinations = @chapters.map { |c| c['destination'] } - [''] + destinations = @chapters.pluck("destination") - [""] items = Item.where(medium: @medium, pdf_destination: destinations, - sort: 'chapter') + sort: "chapter") item_id_map = items.pluck(:pdf_destination, :id).to_h item_destinations = item_id_map.keys - attrs = %i(medium_id pdf_destination section_id sort page - description ref_number position quarantine) + attrs = [:medium_id, :pdf_destination, :section_id, :sort, :page, :description, :ref_number, + :position, :quarantine] item_details = items.pluck(*attrs).map { |i| attrs.zip(i).to_h } contents = [] @chapters.each do |c| contents.push( { medium_id: @medium.id, - pdf_destination: c['destination'], + pdf_destination: c["destination"], section_id: nil, - sort: 'chapter', - page: c['page'].to_i, - description: c['description'], - ref_number: c['label'], + sort: "chapter", + page: c["page"].to_i, + description: c["description"], + ref_number: c["label"], position: nil, quarantine: nil } ) @@ -167,27 +167,27 @@ def create_or_update_chapter_items! end def create_or_update_section_items! - destinations = @sections.map { |s| s['destination'] } - [''] + destinations = @sections.pluck("destination") - [""] items = Item.where(medium: @medium, pdf_destination: destinations, - sort: 'section') + sort: "section") item_id_map = items.pluck(:pdf_destination, :id).to_h item_destinations = item_id_map.keys - attrs = %i(medium_id pdf_destination section_id sort page - description ref_number position quarantine) + attrs = [:medium_id, :pdf_destination, :section_id, :sort, :page, :description, :ref_number, + :position, :quarantine] item_details = items.pluck(*attrs).map { |i| attrs.zip(i).to_h } contents = [] - # note that sections get a position -1 in order to place them ahead + # NOTE: that sections get a position -1 in order to place them ahead # of all content items within themseleves in #script_items_by_position @sections.each do |s| contents.push( { medium_id: @medium.id, - pdf_destination: s['destination'], - section_id: s['mampf_section'].id, - sort: 'section', - page: s['page'].to_i, - description: s['description'], - ref_number: s['label'], + pdf_destination: s["destination"], + section_id: s["mampf_section"].id, + sort: "section", + page: s["page"].to_i, + description: s["description"], + ref_number: s["label"], position: -1, quarantine: nil } ) @@ -200,28 +200,28 @@ def create_or_update_section_items! # in filter_boxes (which basically contains the information on whichk # content checkboxes have been checked) def create_or_update_content_items!(filter_boxes) - destinations = @content.map { |c| c['destination'] } - [''] + destinations = @content.pluck("destination") - [""] items = Item.where(medium: @medium, pdf_destination: destinations) item_id_map = items.pluck(:pdf_destination, :id).to_h item_destinations = item_id_map.keys - attrs = %i(medium_id pdf_destination section_id sort page - description ref_number position hidden quarantine) + attrs = [:medium_id, :pdf_destination, :section_id, :sort, :page, :description, :ref_number, + :position, :hidden, :quarantine] item_details = items.pluck(*attrs).map { |i| attrs.zip(i).to_h } contents = [] @content.each do |c| contents.push( { medium_id: @medium.id, - pdf_destination: c['destination'], + pdf_destination: c["destination"], section_id: @sections.find do |s| - c['section'] == s['section'] - end ['mampf_section']&.id, - sort: Item.internal_sort(c['sort']), - page: c['page'].to_i, - description: c['description'], - ref_number: c['label'], - position: c['counter'], - hidden: filter_boxes[c['counter']].third == false, + c["section"] == s["section"] + end ["mampf_section"]&.id, + sort: Item.internal_sort(c["sort"]), + page: c["page"].to_i, + description: c["description"], + ref_number: c["label"], + position: c["counter"], + hidden: filter_boxes[c["counter"]].third == false, quarantine: nil } ) end @@ -243,7 +243,7 @@ def create_or_update_items!(contents, item_details, item_destinations, @medium.item_ids << new_item_ids changed_contents = different_contents - new_contents changed_contents.each do |c| - Item.find_by_id(item_id_map[c[:pdf_destination]]) + Item.find_by(id: item_id_map[c[:pdf_destination]]) .update(c) end end @@ -255,11 +255,11 @@ def create_or_update_items!(contents, item_details, item_destinations, # be associated with the course of the manuscript's lecture) def update_tags!(filter_boxes) sections_with_content.each do |s| - section = s['mampf_section'] + section = s["mampf_section"] content_in_section(s).each do |c| # if tag for content already exists, add tag to the section and course - if c['tag_id'] - tag = Tag.find_by_id(c['tag_id']) + if c["tag_id"] + tag = Tag.find_by(id: c["tag_id"]) next unless tag next unless section next if section.in?(tag.sections) @@ -268,13 +268,13 @@ def update_tags!(filter_boxes) tag.courses |= [@lecture.course] next end - next unless filter_boxes[c['counter']].second + next unless filter_boxes[c["counter"]].second # if checkbox for tag creation is checked, create the tag, # associate it with course and section tag = Tag.new(courses: [@lecture.course], sections: [section]) - tag.notions.new(title: c['description'], + tag.notions.new(title: c["description"], locale: @lecture.locale || I18n.default_locale) tag.save end @@ -283,8 +283,8 @@ def update_tags!(filter_boxes) # pdf destinations as extracted from pdf metadata def destinations - bookmarks = @medium.manuscript.metadata['bookmarks'] || [] - bookmarks.map { |b| b['destination'] } + bookmarks = @medium.manuscript.metadata["bookmarks"] || [] + bookmarks.pluck("destination") end # pdf destinations together with their multiplicity @@ -307,47 +307,47 @@ def add_info_on_tag_ids .select { |x| x.first.in?(@content_descriptions.map(&:downcase)) } .to_h @content.each do |c| - c['tag_id'] = desc_hash[c['description'].downcase] + c["tag_id"] = desc_hash[c["description"].downcase] end end # add information on the item ids for manuscript content and hidden status def add_info_on_item_ids_and_hidden_status - destinations = @content.map { |c| c['destination'] } - [''] + destinations = @content.pluck("destination") - [""] items_hash = Item.where(medium: @medium, pdf_destination: destinations) .pluck(:pdf_destination, :id, :hidden) - .map { |c| [c[0], [c[1], c[2]]] }.to_h + .to_h { |c| [c[0], [c[1], c[2]]] } @content.each do |c| - c['item_id'] = items_hash[c['destination']]&.first - c['hidden'] = items_hash[c['destination']]&.second + c["item_id"] = items_hash[c["destination"]]&.first + c["hidden"] = items_hash[c["destination"]]&.second end end # private def get_chapters(bookmarks) - bookmarks.select { |b| b['sort'] == @chapter_marker } - .sort_by { |c| c['counter'] } - .each_with_index { |c, i| c['new_position'] = i + 1 } + bookmarks.select { |b| b["sort"] == @chapter_marker } + .sort_by { |c| c["counter"] } + .each_with_index { |c, i| c["new_position"] = i + 1 } end def get_sections(bookmarks) - bookmarks.select { |b| b['sort'] == @section_marker } - .sort_by { |s| s['counter'] } + bookmarks.select { |b| b["sort"] == @section_marker } + .sort_by { |s| s["counter"] } end def get_content(bookmarks) - bookmarks.reject { |b| b['sort'].in?([@chapter_marker, @section_marker]) } - .sort_by { |c| c['counter'] } + bookmarks.reject { |b| b["sort"].in?([@chapter_marker, @section_marker]) } + .sort_by { |c| c["counter"] } end def match_mampf_chapters @chapters.each do |c| mampf_chapter = chapter_in_mampf(c) - c['mampf_chapter'] = mampf_chapter - c['contradiction'] = if mampf_chapter.nil? || - mampf_chapter.title == c['description'] + c["mampf_chapter"] = mampf_chapter + c["contradiction"] = if mampf_chapter.nil? || + mampf_chapter.title == c["description"] false else :different_title @@ -357,16 +357,16 @@ def match_mampf_chapters def match_mampf_sections @sections.each do |s| - bookmarked_chapter_counters = @chapters.map { |c| c['chapter'] } - unless s['chapter'].in?(bookmarked_chapter_counters) - s['mampf_section'] = nil - s['contradiction'] = :missing_chapter + bookmarked_chapter_counters = @chapters.pluck("chapter") + unless s["chapter"].in?(bookmarked_chapter_counters) + s["mampf_section"] = nil + s["contradiction"] = :missing_chapter next end mampf_section = section_in_mampf(s) - s['mampf_section'] = mampf_section - s['contradiction'] = if mampf_section.nil? || - mampf_section.title == s['description'] + s["mampf_section"] = mampf_section + s["contradiction"] = if mampf_section.nil? || + mampf_section.title == s["description"] false else :different_title @@ -375,12 +375,12 @@ def match_mampf_sections end def check_content - bookmarked_section_counters = @sections.map { |s| s['section'] } - bookmarked_chapter_counters = @chapters.map { |c| c['chapter'] } + bookmarked_section_counters = @sections.pluck("section") + bookmarked_chapter_counters = @chapters.pluck("chapter") @content.each do |c| - c['contradiction'] = if !c['chapter'].in?(bookmarked_chapter_counters) + c["contradiction"] = if !c["chapter"].in?(bookmarked_chapter_counters) :missing_chapter - elsif !c['section'].in?(bookmarked_section_counters) + elsif !c["section"].in?(bookmarked_section_counters) :missing_section else false @@ -389,17 +389,17 @@ def check_content end def determine_contradictions - { 'chapters' => @chapters.select { |c| c['contradiction'] }, - 'sections' => @sections.select { |s| s['contradiction'] }, - 'content' => @content.select { |c| c['contradiction'] }, - 'multiplicities' => destinations_with_higher_multiplicities, - 'version' => version_info } + { "chapters" => @chapters.select { |c| c["contradiction"] }, + "sections" => @sections.select { |s| s["contradiction"] }, + "content" => @content.select { |c| c["contradiction"] }, + "multiplicities" => destinations_with_higher_multiplicities, + "version" => version_info } end def determine_contradiction_count - @contradictions['chapters'].size + @contradictions['sections'].size + - @contradictions['content'].size + @contradictions['multiplicities'].size + - @contradictions['version'].size + @contradictions["chapters"].size + @contradictions["sections"].size + + @contradictions["content"].size + @contradictions["multiplicities"].size + + @contradictions["version"].size end def version_info @@ -410,18 +410,17 @@ def version_info # chapters in the manuscript not represented in mampf def new_chapters - @chapters.select { |c| c['mampf_chapter'].nil? } - .map { |c| [c['new_position'], c['description'], c['counter']] } + @chapters.select { |c| c["mampf_chapter"].nil? } + .map { |c| [c["new_position"], c["description"], c["counter"]] } end # sections in a manuscript chapter not represented in mampf def new_sections_in_chapter(chapter) sections = sections_in_chapter(chapter) - sections.each_with_index - .map do |s, i| - [s['mampf_section'], i + 1, s['description'], s['counter']] - end - .select { |s| s.first.nil? } + sections = sections.each_with_index.map do |s, i| + [s["mampf_section"], i + 1, s["description"], s["counter"]] + end + sections.select { |s| s.first.nil? } .map { |s| [s.second, s.third, s.fourth] } end @@ -433,6 +432,6 @@ def sections_with_content # the manuscript as well def existing_tags Notion.where(locale: @lecture.locale || I18n.default_locale) - .pluck('title') & @content_descriptions + .pluck("title") & @content_descriptions end end diff --git a/app/models/medium.rb b/app/models/medium.rb index 27c69505e..0fe6faaa5 100644 --- a/app/models/medium.rb +++ b/app/models/medium.rb @@ -42,11 +42,13 @@ class Medium < ApplicationRecord has_many :imports, dependent: :destroy has_many :importing_lectures, through: :imports, - source: :teachable, source_type: 'Lecture' + source: :teachable, source_type: "Lecture" has_many :importing_courses, through: :imports, - source: :teachable, source_type: 'Course' + source: :teachable, source_type: "Course" - has_many :quiz_certificates, foreign_key: 'quiz_id', dependent: :destroy + has_many :quiz_certificates, foreign_key: "quiz_id", + dependent: :destroy, + inverse_of: :quiz # a medium can be in watchlists of multiple users has_many :watchlist_entries, dependent: :destroy @@ -93,20 +95,17 @@ class Medium < ApplicationRecord # if medium is associated to a nonpublished teachable, reset its published # property to nil before_save :reset_released_status - # some information about media are cached - # to find out whether the cache is out of date, always touch'em after saving - after_save :touch_teachable - # after creation, this creates an item of type 'self' that is just a wrapper # around this medium, so the medium itself can be referenced from other media # as an item as well after_create :create_self_item - # if medium is a question or remark, delete all quiz vertices that refer to it before_destroy :delete_vertices - # if medium is a question, delete all answers that belong to it after_destroy :delete_answers + # some information about media are cached + # to find out whether the cache is out of date, always touch'em after saving + after_save :touch_teachable # keep track of copies (in particular for Questions, Remarks) acts_as_tree @@ -118,13 +117,13 @@ class Medium < ApplicationRecord # locally visible media are published (without inheritance) and unlocked # (they may not be globally visible as their lecture may be unpublished) scope :published, -> { where.not(released: nil) } - scope :locally_visible, -> { where(released: ['all', 'users']) } - scope :potentially_visible, -> { - where(released: ['all', 'users', 'subscribers']) + scope :locally_visible, -> { where(released: ["all", "users"]) } + scope :potentially_visible, lambda { + where(released: ["all", "users", "subscribers"]) } - scope :proper, -> { where.not(sort: 'RandomQuiz') } - scope :expired, -> { - where(sort: 'RandomQuiz').where('created_at < ?', 1.day.ago) + scope :proper, -> { where.not(sort: "RandomQuiz") } + scope :expired, lambda { + where(sort: "RandomQuiz").where("created_at < ?", 1.day.ago) } searchable do @@ -163,50 +162,50 @@ class Medium < ApplicationRecord # these are all the sorts of food(=projects) we currently serve def self.sort_enum - %w[Kaviar Erdbeere Sesam Kiwi Nuesse Script Question Quiz - Reste Remark RandomQuiz] + ["Kaviar", "Erdbeere", "Sesam", "Kiwi", "Nuesse", "Script", "Question", "Quiz", "Reste", + "Remark", "RandomQuiz"] end # media sorts and their descriptions def self.sort_localized - { 'Kaviar' => I18n.t('categories.kaviar.singular'), - 'Sesam' => I18n.t('categories.sesam.singular'), - 'Nuesse' => I18n.t('categories.exercises.singular'), - 'Script' => I18n.t('categories.script.singular'), - 'Kiwi' => I18n.t('categories.kiwi.singular'), - 'Quiz' => I18n.t('categories.quiz.singular'), - 'Question' => I18n.t('categories.question.singular'), - 'Remark' => I18n.t('categories.remark.singular'), - 'RandomQuiz' => I18n.t('categories.randomquiz.singular'), - 'Erdbeere' => I18n.t('categories.erdbeere.singular'), - 'Reste' => I18n.t('categories.reste.singular') } + { "Kaviar" => I18n.t("categories.kaviar.singular"), + "Sesam" => I18n.t("categories.sesam.singular"), + "Nuesse" => I18n.t("categories.exercises.singular"), + "Script" => I18n.t("categories.script.singular"), + "Kiwi" => I18n.t("categories.kiwi.singular"), + "Quiz" => I18n.t("categories.quiz.singular"), + "Question" => I18n.t("categories.question.singular"), + "Remark" => I18n.t("categories.remark.singular"), + "RandomQuiz" => I18n.t("categories.randomquiz.singular"), + "Erdbeere" => I18n.t("categories.erdbeere.singular"), + "Reste" => I18n.t("categories.reste.singular") } end # media sorts and their short descriptions def self.sort_localized_short - { 'Kaviar' => I18n.t('categories.kaviar.short'), - 'Sesam' => I18n.t('categories.sesam.short'), - 'Nuesse' => I18n.t('categories.exercises.short'), - 'Script' => I18n.t('categories.script.short'), - 'Kiwi' => I18n.t('categories.kiwi.short'), - 'Quiz' => I18n.t('categories.quiz.short'), - 'Question' => I18n.t('categories.question.short'), - 'Remark' => I18n.t('categories.remark.short'), - 'RandomQuiz' => I18n.t('categories.randomquiz.short'), - 'Erdbeere' => I18n.t('categories.erdbeere.short'), - 'Reste' => I18n.t('categories.reste.short') } + { "Kaviar" => I18n.t("categories.kaviar.short"), + "Sesam" => I18n.t("categories.sesam.short"), + "Nuesse" => I18n.t("categories.exercises.short"), + "Script" => I18n.t("categories.script.short"), + "Kiwi" => I18n.t("categories.kiwi.short"), + "Quiz" => I18n.t("categories.quiz.short"), + "Question" => I18n.t("categories.question.short"), + "Remark" => I18n.t("categories.remark.short"), + "RandomQuiz" => I18n.t("categories.randomquiz.short"), + "Erdbeere" => I18n.t("categories.erdbeere.short"), + "Reste" => I18n.t("categories.reste.short") } end def self.select_sorts - Medium.sort_localized.except('RandomQuiz').map { |k, v| [v, k] } + Medium.sort_localized.except("RandomQuiz").map { |k, v| [v, k] } end def self.advanced_sorts - ['Question', 'Remark', 'Erdbeere'] + ["Question", "Remark", "Erdbeere"] end def self.generic_sorts - ['Kaviar', 'Sesam', 'Nuesse', 'Script', 'Kiwi', 'Quiz', 'Reste'] + ["Kaviar", "Sesam", "Nuesse", "Script", "Kiwi", "Quiz", "Reste"] end def self.select_generic @@ -214,23 +213,23 @@ def self.select_generic end def self.select_quizzables - Medium.sort_localized.slice('Question', 'Remark').map { |k, v| [v, k] } + Medium.sort_localized.slice("Question", "Remark").map { |k, v| [v, k] } end def self.select_importables - Medium.sort_localized.except('RandomQuiz', 'Question', 'Remark', - 'Manuscript').map { |k, v| [v, k] } + Medium.sort_localized.except("RandomQuiz", "Question", "Remark", + "Manuscript").map { |k, v| [v, k] } end def self.select_question - Medium.sort_localized.slice('Question').map { |k, v| [v, k] } + Medium.sort_localized.slice("Question").map { |k, v| [v, k] } end # returns the array of all media subject to the conditions # provided by the params hash (keys: :id, :project) # :id represents the lecture id def self.search_all(params) - lecture = Lecture.find_by_id(params[:id]) + lecture = Lecture.find_by(id: params[:id]) return Medium.none if lecture.nil? media_in_project = Medium.media_in_project(params[:project]) @@ -246,16 +245,16 @@ def self.search_all(params) # returns the ARel of all media for the given project def self.media_in_project(project) - return Medium.none unless project.present? + return Medium.none if project.blank? - sort = project == 'keks' ? 'Quiz' : project.capitalize + sort = project == "keks" ? "Quiz" : project.capitalize Medium.where(sort: sort) end # returns the array of all media (by title), together with their ids # is used in options_for_select in form helpers. def self.select_by_name - Medium.where.not(sort: ['Question', 'Remark', 'RandomQuiz']) + Medium.where.not(sort: ["Question", "Remark", "RandomQuiz"]) .map { |m| [m.title_for_viewers, m.id] } end @@ -264,30 +263,28 @@ def self.select_by_name # value for :types is an array of integers which correspond to indices # in the sort_enum array def self.search_sorts(search_params) - unless search_params[:all_types] == '0' - return (Medium.sort_enum - ['RandomQuiz']) - end + return (Medium.sort_enum - ["RandomQuiz"]) unless search_params[:all_types] == "0" search_params[:types] || [] end def self.lecture_search_option { - '0' => 'all', - '1' => 'subscribed', - '2' => 'custom' + "0" => "all", + "1" => "subscribed", + "2" => "custom" } end # returns search results for the media search with search_params provided # by the controller - def self.search_by(search_params, page) + def self.search_by(search_params, _page) # If the search is initiated from the start page, you can only get # generic media sorts as results even if the 'all' radio button # is seleted - if search_params[:all_types] == '1' + if search_params[:all_types] == "1" search_params[:types] = - if search_params[:from] == 'start' + if search_params[:from] == "start" Medium.generic_sorts else [] @@ -295,72 +292,76 @@ def self.search_by(search_params, page) end search_params[:teachable_ids] = TeachableParser.new(search_params) .teachables_as_strings - search_params[:editor_ids] = - [] if search_params[:all_editors] == '1' || search_params[:all_editors].nil? + if search_params[:all_editors] == "1" || search_params[:all_editors].nil? + search_params[:editor_ids] = + [] + end # add media without term to current term - search_params[:all_terms] = '1' if search_params[:all_terms].blank? - search_params[:all_teachers] = '1' if search_params[:all_teachers].blank? - search_params[:term_ids].push('0') if search_params[:term_ids].present? - if search_params[:all_tags] == '1' && search_params[:tag_operator] == 'and' + search_params[:all_terms] = "1" if search_params[:all_terms].blank? + search_params[:all_teachers] = "1" if search_params[:all_teachers].blank? + search_params[:term_ids].push("0") if search_params[:term_ids].present? + if search_params[:all_tags] == "1" && search_params[:tag_operator] == "and" search_params[:tag_ids] = Tag.pluck(:id) end user = User.find_by(id: search_params[:user_id]) search = Sunspot.new_search(Medium) search.build do with(:sort, search_params[:types]) - without(:sort, 'RandomQuiz') + without(:sort, "RandomQuiz") without(:sort, Medium.advanced_sorts) unless user&.admin_or_editor? with(:editor_ids, search_params[:editor_ids]) with(:teachable_compact, search_params[:teachable_ids]) - with(:term_id, - search_params[:term_ids]) unless search_params[:all_terms] == '1' - with(:teacher_id, - search_params[:teacher_ids]) unless search_params[:all_teachers] == '1' + unless search_params[:all_terms] == "1" + with(:term_id, + search_params[:term_ids]) + end + unless search_params[:all_teachers] == "1" + with(:teacher_id, + search_params[:teacher_ids]) + end end - if search_params[:purpose] == 'clicker' + if search_params[:purpose] == "clicker" search.build do with(:clickerizable, true) end end - unless search_params[:answers_count] == 'irrelevant' + unless search_params[:answers_count] == "irrelevant" search.build do with(:answers_count, [-1, search_params[:answers_count].to_i]) end end - unless search_params[:access] == 'irrelevant' + unless search_params[:access] == "irrelevant" search.build do with(:release_state, search_params[:access]) end end - unless search_params[:all_tags] == '1' && - search_params[:tag_operator] == 'or' - if search_params[:tag_ids] - if search_params[:tag_operator] == 'or' || search_params[:all_tags] == '1' - search.build do - with(:tag_ids).any_of(search_params[:tag_ids]) - end - else - search.build do - with(:tag_ids).all_of(search_params[:tag_ids]) - end + if !search_params[:all_tags] == "1" && + !search_params[:tag_operator] == "or" && (search_params[:tag_ids]) + if search_params[:tag_operator] == "or" || search_params[:all_tags] == "1" + search.build do + with(:tag_ids).any_of(search_params[:tag_ids]) + end + else + search.build do + with(:tag_ids).all_of(search_params[:tag_ids]) end end end if search_params[:fulltext].present? search.build do - fulltext search_params[:fulltext] do - boost_fields :description => 2.0 + fulltext(search_params[:fulltext]) do + boost_fields(description: 2.0) end end end if search_params[:lecture_option].present? case Medium.lecture_search_option[search_params[:lecture_option]] - when 'subscribed' + when "subscribed" search.build do with(:subscribed_users, search_params[:user_id]) end - when 'custom' + when "custom" search.build do with(:lecture, search_params[:media_lectures]) end @@ -368,7 +369,7 @@ def self.search_by(search_params, page) end # this is needed for kaminari to function correctly search.build do - paginate page: 1, per_page: Medium.all.count + paginate(page: 1, per_page: Medium.count) end search end @@ -376,10 +377,10 @@ def self.search_by(search_params, page) def self.search_questions_by_tags(search_params) search = Sunspot.new_search(Medium) search.build do - with(:sort, 'Question') + with(:sort, "Question") with(:teachable_compact, search_params[:teachable_ids]) with(:tag_ids).all_of(search_params[:tag_ids]) - paginate per_page: Question.count + paginate(per_page: Question.count) end search end @@ -392,12 +393,6 @@ def self.similar_courses(search_string) end end - def restricted? - return false unless teachable - - teachable.restricted? - end - # protected items are items of type 'pdf_destination' inside associated to # this medium that are referred to from other media or from an entry # within the table of contents of the video associated to this medium. @@ -405,17 +400,17 @@ def restricted? # the user insists (this way they are protected for example in the situation # where the user temporarily commented out some part of the manuscript) def protected_items - return [] unless sort == 'Script' + return [] unless sort == "Script" pdf_items = Item.where(medium: self).where.not(pdf_destination: nil) Referral.where(item: pdf_items).map(&:item).uniq end def vanished_items - return [] unless sort == 'Script' + return [] unless sort == "Script" Item.where(medium: self) - .where.not(sort: 'self') + .where.not(sort: "self") .where.not(pdf_destination: manuscript_destinations) end @@ -443,11 +438,11 @@ def quarantine # - put items that correspond to missing destination in quarantine (and # return these) def update_pdf_destinations! - return unless sort == 'Script' + return unless sort == "Script" irrelevant_items.delete_all result = missing_items_outside_quarantine.pluck(:pdf_destination) - missing_items_outside_quarantine.update_all(quarantine: true) + missing_items_outside_quarantine.update(quarantine: true) result end @@ -466,15 +461,15 @@ def edited_with_inheritance_by?(user) return true if teachable&.lecture&.editors&.include?(user) return true if teachable&.lecture&.teacher == user return true if teachable&.course&.editors&.include?(user) - return true if teachable&.is_a?(Talk) && user.in?(teachable.speakers) + return true if teachable.is_a?(Talk) && user.in?(teachable.speakers) false end def editors_with_inheritance - return [] if sort == 'RandomQuiz' + return [] if sort == "RandomQuiz" - result = (editors&.to_a + teachable.lecture&.editors.to_a + + result = (editors&.to_a&.+ teachable.lecture&.editors.to_a + [teachable.lecture&.teacher] + teachable.course.editors.to_a).uniq.compact return result unless teachable.is_a?(Talk) @@ -486,9 +481,7 @@ def editors_with_inheritance def eligible_editors(user) result = editors_with_inheritance - if teachable.is_a?(Talk) && user.can_edit?(lecture) - result.concat(lecture.speakers) - end + result.concat(lecture.speakers) if teachable.is_a?(Talk) && user.can_edit?(lecture) result << user if user.admin? result.uniq @@ -497,11 +490,11 @@ def eligible_editors(user) # creates a .vtt tmp file (and returns it), which contains # all data needed by the thyme player to realize the toc def toc_to_vtt - file = Tempfile.new(['toc-', '.vtt'], encoding: 'UTF-8') - file.write vtt_start + file = Tempfile.new(["toc-", ".vtt"], encoding: "UTF-8") + file.write(vtt_start) proper_items_by_time.reject(&:hidden).each do |i| - file.write i.vtt_time_span - file.write i.vtt_reference + file.write(i.vtt_time_span) + file.write(i.vtt_reference) end file end @@ -510,12 +503,12 @@ def toc_to_vtt # all data needed by the thyme player to realize references # Note: Only references to unlocked media will be incorporated. def references_to_vtt - file = Tempfile.new(['ref-', '.vtt'], encoding: 'UTF-8') - file.write vtt_start + file = Tempfile.new(["ref-", ".vtt"], encoding: "UTF-8") + file.write(vtt_start) referrals_by_time.select { |r| r.item_published? && !r.item_locked? } .each do |r| - file.write r.vtt_time_span - file.write JSON.pretty_generate(r.vtt_properties) + "\n\n" + file.write(r.vtt_time_span) + file.write("#{JSON.pretty_generate(r.vtt_properties)}\n\n") end file end @@ -528,7 +521,7 @@ def create_vtt_container! # some plain methods for items and referrals def proper_items - items.where.not(sort: ['self', 'pdf_destination']) + items.where.not(sort: ["self", "pdf_destination"]) .where.not(start_time: nil) end @@ -547,11 +540,11 @@ def referrals_by_time def screenshot_url_with_host return screenshot_url(host: host) unless screenshot(:normalized) - return screenshot_url(:normalized, host: host) + screenshot_url(:normalized, host: host) end def video_url - return unless video.present? + return if video.blank? video.url(host: host) end @@ -561,45 +554,45 @@ def video_download_url end def video_filename - return unless video.present? + return if video.blank? - video.metadata['filename'] + video.metadata["filename"] end def video_size - return unless video.present? + return if video.blank? - video.metadata['size'] + video.metadata["size"] end def video_resolution - return unless video.present? + return if video.blank? - video.metadata['resolution'] + video.metadata["resolution"] end def video_duration - return unless video.present? + return if video.blank? - video.metadata['duration'] + video.metadata["duration"] end def video_duration_hms_string - return unless video.present? + return if video.blank? TimeStamp.new(total_seconds: video_duration).hms_string end def geogebra_filename - return unless geogebra.present? + return if geogebra.blank? - geogebra.metadata['filename'] + geogebra.metadata["filename"] end def geogebra_size - return unless geogebra.present? + return if geogebra.blank? - geogebra.metadata['size'] + geogebra.metadata["size"] end def geogebra_url_with_host @@ -611,13 +604,13 @@ def geogebra_download_url end def geogebra_screenshot_url - return '' unless geogebra.present? + return "" if geogebra.blank? geogebra_url(:screenshot, host: host) end def manuscript_url_with_host - return manuscript_url(host: host) + "/" + manuscript_filename if ENV["REWRITE_ENABLED"] == "1" + return "#{manuscript_url(host: host)}/#{manuscript_filename}" if ENV["REWRITE_ENABLED"] == "1" manuscript_url(host: host) end @@ -627,45 +620,45 @@ def manuscript_download_url end def manuscript_filename - return unless manuscript.present? + return if manuscript.blank? - return manuscript.metadata['filename'] + manuscript.metadata["filename"] end def manuscript_size - return unless manuscript.present? + return if manuscript.blank? - return manuscript.metadata['size'] + manuscript.metadata["size"] end def manuscript_pages - return unless manuscript.present? + return if manuscript.blank? - return manuscript.metadata['pages'] + manuscript.metadata["pages"] end def manuscript_screenshot_url - return '' unless manuscript.present? + return "" if manuscript.blank? manuscript_url(:screenshot, host: host) end def manuscript_destinations - return [] unless manuscript.present? && sort == 'Script' + return [] unless manuscript.present? && sort == "Script" - manuscript.metadata['destinations'] || [] + manuscript.metadata["destinations"] || [] end def video_width - return unless video.present? + return if video.blank? - video_resolution.split('x')[0].to_i + video_resolution.split("x")[0].to_i end def video_height - return unless video.present? + return if video.blank? - video_resolution.split('x')[1].to_i + video_resolution.split("x")[1].to_i end def video_aspect_ratio @@ -682,16 +675,14 @@ def video_scaled_height(new_width) def caption return description if description.present? - return '' unless sort == 'Kaviar' && teachable_type == 'Lesson' + return "" unless sort == "Kaviar" && teachable_type == "Lesson" - teachable.section_titles || '' + teachable.section_titles || "" end # methods that create card header and subheader for a medium card - def card_header - teachable.card_header - end + delegate :card_header, to: :teachable def card_header_teachable_path(user) teachable.card_header_path(user) @@ -702,9 +693,9 @@ def card_subheader end def card_tooltip - return Medium.sort_localized[sort] unless sort == 'Nuesse' && file_last_edited + return Medium.sort_localized[sort] unless sort == "Nuesse" && file_last_edited - I18n.t('categories.exercises.singular_updated') + I18n.t("categories.exercises.singular_updated") end def sort_localized @@ -712,12 +703,13 @@ def sort_localized end def subheader_style - return 'badge bg-secondary' unless sort == 'Nuesse' && file_last_edited - 'badge bg-danger' + return "badge bg-secondary" unless sort == "Nuesse" && file_last_edited + + "badge bg-danger" end def cache_key - super + '-' + I18n.locale.to_s + "#{super}-#{I18n.locale}" end def published? @@ -725,23 +717,23 @@ def published? end def locked? - released == 'locked' + released == "locked" end def restricted? - released == 'subscribers' + released == "subscribers" end def free? - released == 'all' + released == "all" end def for_users? - released == 'users' + released == "users" end def visible? - released.in?(['all', 'users', 'subscribers']) + released.in?(["all", "users", "subscribers"]) end def visible_for_user?(user) @@ -750,12 +742,12 @@ def visible_for_user?(user) return false unless published? return false if locked? - if teachable_type == 'Course' - return false if restricted? && !teachable.in?(user.courses) - end - if teachable_type.in?(['Lecture', 'Lesson', 'Talk']) - return false if restricted? && !teachable.lecture.in?(user.lectures) + return false if teachable_type == "Course" && (restricted? && !teachable.in?(user.courses)) + if teachable_type.in?(["Lecture", "Lesson", + "Talk"]) && (restricted? && !teachable.lecture.in?(user.lectures)) + return false end + true end @@ -783,9 +775,9 @@ def irrelevant? end def teachable_select - return nil unless teachable.present? + return nil if teachable.blank? - teachable_type + '-' + teachable_id.to_s + "#{teachable_type}-#{teachable_id}" end # media associated to the same teachable and of the same sort @@ -799,7 +791,7 @@ def siblings def compact_info_uncached return "#{sort_localized}.#{teachable.compact_title}" unless quizzy? - "#{sort_localized}.#{teachable.compact_title}.\##{id}" + "#{sort_localized}.#{teachable.compact_title}.##{id}" end def compact_info @@ -814,17 +806,17 @@ def compact_info def local_info_uncached return description if description.present? - return I18n.t('admin.medium.local_info.no_title') unless undescribable? + return I18n.t("admin.medium.local_info.no_title") unless undescribable? - if sort == 'Kaviar' && teachable_type == 'Lesson' - return I18n.t('admin.medium.local_info.to_session', + if sort == "Kaviar" && teachable_type == "Lesson" + return I18n.t("admin.medium.local_info.to_session", number: teachable.number, date: teachable.date_localized) - elsif sort == 'Script' - return I18n.t('categories.script.singular') + elsif sort == "Script" + return I18n.t("categories.script.singular") end - "#{sort_localized} \##{id}" + "#{sort_localized} ##{id}" end def local_info @@ -836,7 +828,7 @@ def local_info def local_info_for_admins_uncached return local_info unless quizzy? - "\##{id}.#{local_info}" + "##{id}.#{local_info}" end def local_info_for_admins @@ -848,12 +840,10 @@ def local_info_for_admins # returns description if present, otherwise '' def details_uncached - return description unless description.blank? - unless undescribable? - return "#{I18n.t('admin.medium.local_info.no_title')}.ID#{id}" - end + return description if description.present? + return "#{I18n.t("admin.medium.local_info.no_title")}.ID#{id}" unless undescribable? - '' + "" end def details @@ -862,12 +852,6 @@ def details end end - def title_uncached - return compact_info if details.blank? - - compact_info + '.' + details - end - def title Rails.cache.fetch("#{cache_key_with_version}/title") do title_uncached @@ -877,8 +861,8 @@ def title # returns info made from sort, teachable title and description def title_for_viewers_uncached - sort_localized + ', ' + teachable&.title_for_viewers.to_s + - (description.present? ? ', ' + description : '') + description_str = description.present? ? ", #{description}" : "" + "#{sort_localized}, #{teachable&.title_for_viewers}#{description_str}" end def title_for_viewers @@ -893,16 +877,6 @@ def scoped_teachable_title end end - # returns info made from sort and description - def local_title_for_viewers_uncached - return "#{sort_localized}, #{description}" if description.present? - if sort == 'Kaviar' && teachable.class.to_s == 'Lesson' - return "#{I18n.t('categories.kaviar.singular')}, #{teachable.local_title_for_viewers}" - end - - "#{sort_localized}, #{I18n.t('admin.medium.local_info.no_title')}" - end - # returns info made from sort and description def local_title_for_viewers Rails.cache.fetch("#{cache_key_with_version}/local_title_for_viewers") do @@ -913,7 +887,7 @@ def local_title_for_viewers # this is used in dropdowns for compact info def extended_label Rails.cache.fetch("#{cache_key_with_version}/extended_label") do - "#{teachable.compact_title}.\##{id}.#{description}" + "#{teachable.compact_title}.##{id}.#{description}" end end @@ -937,7 +911,7 @@ def items_with_references end def proper? - return true unless sort == 'RandomQuiz' + return true unless sort == "RandomQuiz" false end @@ -947,23 +921,23 @@ def locale_with_inheritance end def sanitize_type! - update(type: 'Quiz') if sort.in?(['Quiz', 'RandomQuiz']) - update(type: sort) if sort.in?(['Question', 'Remark']) - update(type: nil) if !sort.in?(['Quiz', 'Question', 'Remark', 'RandomQuiz']) + update(type: "Quiz") if sort.in?(["Quiz", "RandomQuiz"]) + update(type: sort) if sort.in?(["Question", "Remark"]) + update(type: nil) unless sort.in?(["Quiz", "Question", "Remark", "RandomQuiz"]) end def select_sorts result = if new_record? - Medium.sort_localized.except('RandomQuiz') - elsif sort.in?(['Kaviar', 'Sesam', 'Erdbeere', 'Kiwi', 'Nuesse', - 'Reste']) - Medium.sort_localized.except('RandomQuiz', 'Script', 'Quiz', - 'Question', 'Remark') + Medium.sort_localized.except("RandomQuiz") + elsif sort.in?(["Kaviar", "Sesam", "Erdbeere", "Kiwi", "Nuesse", + "Reste"]) + Medium.sort_localized.except("RandomQuiz", "Script", "Quiz", + "Question", "Remark") else Medium.sort_localized.slice(sort) end - if teachable_type == 'Talk' - result.except!('RandomQuiz', 'Question', 'Remark', 'Erdbeere', 'Script') + if teachable_type == "Talk" + result.except!("RandomQuiz", "Question", "Remark", "Erdbeere", "Script") end result.map { |k, v| [v, k] } end @@ -973,12 +947,12 @@ def select_sorts_with_self end def extracted_linked_media - video_links = Medium.where(id: referenced_items.where(sort: 'self') + video_links = Medium.where(id: referenced_items.where(sort: "self") .where.not(medium: nil) .pluck(:medium_id)) - return video_links unless manuscript.present? + return video_links if manuscript.blank? - manuscript_media_ids = manuscript.metadata['linked_media'] || [] + manuscript_media_ids = manuscript.metadata["linked_media"] || [] manuscript_links = Medium.where(id: manuscript_media_ids) video_links.or(manuscript_links) end @@ -994,38 +968,38 @@ def linked_media_ids_cached end def toc_items - return [] unless sort == 'Script' + return [] unless sort == "Script" - items.where(sort: ['chapter', 'section']) + items.where(sort: ["chapter", "section"]) .natural_sort_by { |x| [x.page, x.ref_number] } end def tags_outside_lesson - return Tag.none unless teachable_type == 'Lesson' + return Tag.none unless teachable_type == "Lesson" tags.where.not(id: teachable.tag_ids) end def extended_content result = [] - if teachable_type == 'Lesson' && teachable.details.present? - result.push I18n.t('admin.medium.lesson_details_html') + teachable.details + if teachable_type == "Lesson" && teachable.details.present? + result.push(I18n.t("admin.medium.lesson_details_html") + teachable.details) end - result.push content unless content.blank? + result.push(content) if content.present? result end def script_items_importable? - return unless teachable_type == 'Lesson' - return unless teachable.lecture.content_mode == 'manuscript' - return unless teachable.script_items.any? + return false unless teachable_type == "Lesson" + return false unless teachable.lecture.content_mode == "manuscript" + return false unless teachable.script_items.any? true end def import_script_items! - return unless teachable_type == 'Lesson' - return unless teachable.lecture.content_mode == 'manuscript' + return unless teachable_type == "Lesson" + return unless teachable.lecture.content_mode == "manuscript" items = teachable.script_items return unless items.any? @@ -1073,55 +1047,48 @@ def planned_comment_lock? end def becomes_quizzable - return unless type.in?(['Question', 'Remark']) - return becomes(Question) if type == 'Question' + return unless type.in?(["Question", "Remark"]) + return becomes(Question) if type == "Question" becomes(Remark) end - def containingWatchlists(user) - Watchlist.where(id: WatchlistEntry.where(medium: self).pluck(:watchlist_id), + def containing_watchlists(user) + Watchlist.where(id: WatchlistEntry.where(medium: self).select(:watchlist_id), user: user) end - def containingWatchlistsNames(user) - watchlists = containingWatchlists(user) - if !watchlists.empty? - containingWatchlists(user).pluck(:name) + def containing_watchlists_names(user) + watchlists = containing_watchlists(user) + if watchlists.empty? + "" else - '' + watchlists.pluck(:name) end end def collects_statistics - video.present? || manuscript.present? || sort == 'Quiz' + video.present? || manuscript.present? || sort == "Quiz" end def term_id - teachable.term_id if teachable.class.to_s == 'Lecture' - return unless teachable.class.to_s == 'Lesson' + teachable.term_id if teachable.instance_of?(::Lecture) + return unless teachable.instance_of?(::Lesson) Lecture.find_by(id: teachable.lecture_id).term_id end def supervising_teacher_id - return teachable.teacher_id if teachable.class.to_s == 'Lecture' - return unless teachable.class.to_s == 'Lesson' - - Lecture.find_by(id: teachable.lecture_id).teacher_id - end - - def supervising_teacher_id - return teachable.teacher_id if teachable.class.to_s == 'Lecture' - return unless teachable.class.to_s == 'Lesson' + return teachable.teacher_id if teachable.instance_of?(::Lecture) + return unless teachable.instance_of?(::Lesson) Lecture.find_by(id: teachable.lecture_id).teacher_id end def subscribed_users - return teachable.user_ids if ['Lecture', - 'Course'].include? teachable.class.to_s - return unless teachable.class.to_s == 'Lesson' + return teachable.user_ids if ["Lecture", + "Course"].include?(teachable.class.to_s) + return unless teachable.instance_of?(::Lesson) Lecture.find_by(id: teachable.lecture_id).user_ids end @@ -1131,35 +1098,34 @@ def subscribed_users # media of type kaviar associated to a lesson and script do not require # a description def undescribable? - (sort == 'Kaviar' && teachable.class.to_s == 'Lesson') || - sort == 'Script' + (sort == "Kaviar" && teachable.instance_of?(::Lesson)) || + sort == "Script" end def quizzy? - sort.in?(['Quiz', 'Question', 'Remark']) + sort.in?(["Quiz", "Question", "Remark"]) end def title_uncached return compact_info if details.blank? - compact_info + '.' + details + "#{compact_info}.#{details}" end + # returns info made from sort and description def local_title_for_viewers_uncached return "#{sort_localized}, #{description}" if description.present? - if sort == 'Kaviar' && teachable.class.to_s == 'Lesson' - return "#{I18n.t('categories.kaviar.singular')}, #{teachable.local_title_for_viewers}" + if sort == "Kaviar" && teachable.instance_of?(::Lesson) + return "#{I18n.t("categories.kaviar.singular")}, #{teachable.local_title_for_viewers}" end - "#{sort_localized}, #{I18n.t('admin.medium.local_info.no_title')}" + "#{sort_localized}, #{I18n.t("admin.medium.local_info.no_title")}" end def touch_teachable return if teachable.nil? - if teachable.course.present? && teachable.course.persisted? - teachable.course.touch - end + teachable.course.touch if teachable.course.present? && teachable.course.persisted? optional_touches end @@ -1170,12 +1136,8 @@ def reset_released_status end def optional_touches - if teachable.lecture.present? && teachable.lecture.persisted? - teachable.lecture.touch - end - if teachable.lesson.present? && teachable.lesson.persisted? - teachable.lesson.touch - end + teachable.lecture.touch if teachable.lecture.present? && teachable.lecture.persisted? + teachable.lesson.touch if teachable.lesson.present? && teachable.lesson.persisted? return unless teachable.talk.present? && teachable.talk.persisted? teachable.talk.touch @@ -1186,34 +1148,34 @@ def vtt_start end def belongs_to_course?(lecture) - teachable_type == 'Course' && teachable == lecture.course + teachable_type == "Course" && teachable == lecture.course end def belongs_to_lecture?(lecture) - teachable_type == 'Lecture' && teachable == lecture + teachable_type == "Lecture" && teachable == lecture end def belongs_to_lesson?(lecture) - teachable_type == 'Lesson' && teachable.lecture == lecture + teachable_type == "Lesson" && teachable.lecture == lecture end def create_self_item - return if sort.in?(['Question', 'Remark', 'RandomQuiz']) + return if sort.in?(["Question", "Remark", "RandomQuiz"]) - Item.create(sort: 'self', medium: self) + Item.create(sort: "self", medium: self) end def local_items - return teachable.items - items if teachable_type == 'Course' + return teachable.items - items if teachable_type == "Course" teachable.lecture.items - items end def at_most_one_manuscript - return true unless teachable_type == 'Lecture' - return true unless sort == 'Script' + return true unless teachable_type == "Lecture" + return true unless sort == "Script" - if (Medium.where(sort: 'Script', + if (Medium.where(sort: "Script", teachable: teachable).to_a - [self]).size.positive? errors.add(:sort, :lecture_manuscript_exists) return false @@ -1222,27 +1184,27 @@ def at_most_one_manuscript end def script_only_for_lectures - return true if teachable_type == 'Lecture' - return true unless sort == 'Script' + return true if teachable_type == "Lecture" + return true unless sort == "Script" errors.add(:sort, :lecture_only) false end def no_video_for_script - return true unless sort == 'Script' - return true unless video.present? + return true unless sort == "Script" + return true if video.blank? errors.add(:sort, :no_video) false end def no_changing_sort_to_or_from_script - if sort_was == 'Script' && sort != 'Script' + if sort_was == "Script" && sort != "Script" errors.add(:sort, :no_conversion_from_script) return false end - if persisted? && sort_was != 'Script' && sort == 'Script' + if persisted? && sort_was != "Script" && sort == "Script" errors.add(:sort, :no_conversion_to_script) return false end @@ -1250,16 +1212,16 @@ def no_changing_sort_to_or_from_script end def no_tags_for_scripts - return true unless sort == 'Script' && tags.any? + return true unless sort == "Script" && tags.any? errors.add(:tags, :no_tags_allowed) false end def delete_vertices - return unless type.in?(['Question', 'Remark']) + return unless type.in?(["Question", "Remark"]) - if type == 'Question' + if type == "Question" becomes(Question).delete_vertices return end @@ -1267,26 +1229,26 @@ def delete_vertices end def delete_answers - return unless type == 'Question' + return unless type == "Question" becomes(Question).answers.delete_all end def text_join - return unless type.in?(['Question', 'Remark']) - return text if type == 'Remark' + return unless type.in?(["Question", "Remark"]) + return text if type == "Remark" - "#{text} #{becomes(Question).answers&.map(&:text_join)&.join(' ')}" + "#{text} #{becomes(Question).answers&.map(&:text_join)&.join(" ")}" end def release_state return released unless released.nil? - 'unpublished' + "unpublished" end def clickerizable? - return false unless type == 'Question' + return false unless type == "Question" question = becomes(Question) return false unless question.answers.count.in?((2..6)) @@ -1295,7 +1257,7 @@ def clickerizable? end def answers_count - return -1 unless type == 'Question' + return -1 unless type == "Question" becomes(Question).answers.count end diff --git a/app/models/medium_publisher.rb b/app/models/medium_publisher.rb index 1d5923632..3607359e3 100644 --- a/app/models/medium_publisher.rb +++ b/app/models/medium_publisher.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # PORO class that handles the publication of media class MediumPublisher attr_reader :medium_id, :user_id, :release_now, :release_for, :release_date, @@ -7,11 +5,11 @@ class MediumPublisher :assignment_file_type, :assignment_deadline, :assignment_deletion_date - def initialize(medium_id:, user_id:, release_now:, - release_for: 'all', release_date: nil, + def initialize(medium_id:, user_id:, release_now:, # rubocop:todo Metrics/ParameterLists + release_for: "all", release_date: nil, lock_comments: false, vertices: false, - create_assignment: false, assignment_title: '', - assignment_file_type: '', assignment_deadline: nil, + create_assignment: false, assignment_title: "", + assignment_file_type: "", assignment_deadline: nil, assignment_deletion_date: nil) @medium_id = medium_id @user_id = user_id @@ -45,27 +43,27 @@ def self.dump(medium_publisher) def self.parse(medium, user, params) begin - release_date = Time.zone.parse(params[:release_date] || '') + release_date = Time.zone.parse(params[:release_date] || "") rescue ArgumentError - puts 'Argument error for medium release date' + Rails.logger.debug("Argument error for medium release date") end begin - assignment_deadline = Time.zone.parse(params[:assignment_deadline] || '') + assignment_deadline = Time.zone.parse(params[:assignment_deadline] || "") rescue ArgumentError - puts 'Argument error for medium assignment deadline' + Rails.logger.debug("Argument error for medium assignment deadline") end begin - assignment_deletion_date = Time.zone.parse(params[:assignment_deletion_date] || '') + assignment_deletion_date = Time.zone.parse(params[:assignment_deletion_date] || "") rescue ArgumentError - puts 'Argument error for medium assignment deletion date' + Rails.logger.debug("Argument error for medium assignment deletion date") end MediumPublisher.new(medium_id: medium.id, user_id: user.id, - release_now: params[:release_now] == '1', + release_now: params[:release_now] == "1", release_for: params[:released], release_date: release_date, - lock_comments: params[:lock_comments] == '1', - vertices: params[:publish_vertices] == '1', - create_assignment: params[:create_assignment] == '1', + lock_comments: params[:lock_comments] == "1", + vertices: params[:publish_vertices] == "1", + create_assignment: params[:create_assignment] == "1", assignment_title: params[:assignment_title], assignment_file_type: params[:assignment_file_type], assignment_deadline: assignment_deadline, @@ -73,8 +71,8 @@ def self.parse(medium, user, params) end def publish! - @medium = Medium.find_by_id(@medium_id) - @user = User.find_by_id(@user_id) + @medium = Medium.find_by(id: @medium_id) + @user = User.find_by(id: @user_id) return unless @medium && @user && @medium.released_at.nil? return unless @user.can_edit?(@medium) @@ -122,7 +120,7 @@ def update_medium! def realize_optional_stuff! close_thread! if @lock_comments - publish_vertices! if @medium.sort == 'Quiz' && @vertices + publish_vertices! if @medium.sort == "Quiz" && @vertices create_assignment! if @create_assignment end @@ -131,12 +129,12 @@ def realize_optional_stuff! def create_notifications! @medium.teachable&.media_scope&.touch notifications = [] - @medium.teachable.media_scope.users.update_all(updated_at: Time.zone.now) + @medium.teachable.media_scope.users.touch_all @medium.teachable.media_scope.users.each do |u| notifications << Notification.new(recipient: u, notifiable_id: @medium.id, - notifiable_type: 'Medium', - action: 'create') + notifiable_type: "Medium", + action: "create") end Notification.import notifications end @@ -156,7 +154,7 @@ def send_notification_email! end def medium - Medium.find_by_id(@medium_id) + Medium.find_by(id: @medium_id) end def publish_vertices! @@ -191,27 +189,27 @@ def invalid_assignment_title? end def add_release_date_error - @errors[:release_date] = I18n.t('admin.medium.invalid_publish_date') + @errors[:release_date] = I18n.t("admin.medium.invalid_publish_date") end def add_assignment_deadline_error - @errors[:assignment_deadline] = I18n.t('admin.medium' \ - '.invalid_assignment_deadline') + @errors[:assignment_deadline] = I18n.t("admin.medium" \ + ".invalid_assignment_deadline") end def add_assignment_deletion_date_error - @errors[:assignment_deletion_date] = I18n.t('activerecord.errors.' \ - 'models.assignment.' \ - 'attributes.deletion_date.' \ - 'in_past') + @errors[:assignment_deletion_date] = I18n.t("activerecord.errors." \ + "models.assignment." \ + "attributes.deletion_date." \ + "in_past") end def add_assignment_title_error - @errors[:assignment_title] = I18n.t('admin.medium' \ - '.invalid_assignment_title') + @errors[:assignment_title] = I18n.t("admin.medium" \ + ".invalid_assignment_title") end def medium_without_notifications? - @medium.sort.in?(['Question', 'Remark', 'RandomQuiz']) + @medium.sort.in?(["Question", "Remark", "RandomQuiz"]) end end diff --git a/app/models/notification.rb b/app/models/notification.rb index 278066c89..ee62b890c 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -4,7 +4,7 @@ class Notification < ApplicationRecord include ActionDispatch::Routing::PolymorphicRoutes include Rails.application.routes.url_helpers - belongs_to :recipient, class_name: 'User', touch: true + belongs_to :recipient, class_name: "User", touch: true belongs_to :notifiable, polymorphic: true, optional: true paginates_per 12 @@ -19,9 +19,9 @@ class Notification < ApplicationRecord # returns the lecture associated to a notification of type announcement, # and teachable for a notification of type medium, nil otherwise def teachable - return unless notifiable.present? - return if notifiable_type.in?(['Lecture', 'Course']) - return notifiable.lecture if notifiable_type == 'Announcement' + return if notifiable.blank? + return if notifiable_type.in?(["Lecture", "Course"]) + return notifiable.lecture if notifiable_type == "Announcement" # notifiable will be a medium, so return its teachable notifiable.teachable @@ -33,49 +33,47 @@ def teachable # news path for general announcements # all other cases: notifiable path def path(user) - return unless notifiable.present? - return edit_profile_path if notifiable_type.in?(['Course', 'Lecture']) + return if notifiable.blank? + return edit_profile_path if notifiable_type.in?(["Course", "Lecture"]) - if notifiable_type == 'Announcement' + if notifiable_type == "Announcement" return notifiable.lecture.path(user) if notifiable.lecture.present? return news_path end - if notifiable_type == 'Medium' && notifiable.sort == 'Quiz' - return medium_path(notifiable) - end + return medium_path(notifiable) if notifiable_type == "Medium" && notifiable.sort == "Quiz" polymorphic_url(notifiable, only_path: true) end def self.allowed_notifiable_types - ['Medium', 'Course', 'Lecture', 'Announcement'] + ["Medium", "Course", "Lecture", "Announcement"] end # the next methods are for the determination which kind of notification it is def medium? - return unless notifiable.present? + return false if notifiable.blank? - notifiable_type == 'Medium' + notifiable_type == "Medium" end def course? - return unless notifiable.present? + return false if notifiable.blank? - notifiable.class.to_s == 'Course' + notifiable.instance_of?(::Course) end def lecture? - return unless notifiable.present? + return false if notifiable.blank? - notifiable.class.to_s == 'Lecture' + notifiable.instance_of?(::Lecture) end def announcement? - return unless notifiable.present? + return false if notifiable.blank? - notifiable.class.to_s == 'Announcement' + notifiable.instance_of?(::Announcement) end def generic_announcement? diff --git a/app/models/notion.rb b/app/models/notion.rb index de98a93d9..15d339ef9 100644 --- a/app/models/notion.rb +++ b/app/models/notion.rb @@ -1,13 +1,13 @@ class Notion < ApplicationRecord belongs_to :tag, optional: true, touch: true - belongs_to :aliased_tag, class_name: 'Tag', optional: true, touch: true + belongs_to :aliased_tag, class_name: "Tag", optional: true, touch: true - validates :title, uniqueness: { scope: :locale } + validates :title, uniqueness: { scope: :locale } # rubocop:todo Rails/UniqueValidationWithoutIndex validates :title, presence: true validate :presence_of_tag, if: :persisted? - after_save :touch_tag_relations before_destroy :touch_tag_relations + after_save :touch_tag_relations def presence_of_tag return if tag || aliased_tag diff --git a/app/models/probe.rb b/app/models/probe.rb index 59b892754..7de065442 100644 --- a/app/models/probe.rb +++ b/app/models/probe.rb @@ -2,7 +2,7 @@ class Probe < InteractionsRecord scope :created_between, lambda { |start_date, end_date| where(created_at: start_date.beginning_of_day..end_date.end_of_day) } - require 'csv' + require "csv" def self.finished_quizzes(quiz) Probe.where(quiz_id: quiz.id, progress: -1).count @@ -46,8 +46,8 @@ def self.local_success_in_quiz(quiz) end def self.to_csv - attributes = %w{id session_id created_at quiz_id question_id remark_id - correct progress success study_participant input} + attributes = ["id", "session_id", "created_at", "quiz_id", "question_id", "remark_id", + "correct", "progress", "success", "study_participant", "input"] CSV.generate(headers: true) do |csv| csv << attributes diff --git a/app/models/question.rb b/app/models/question.rb index 8d3eb7535..521c20ac6 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -26,11 +26,11 @@ def quiz_ids end def proper_quiz_ids - Quiz.where(id: quiz_ids, sort: 'Quiz').pluck(:id) + Quiz.where(id: quiz_ids, sort: "Quiz").pluck(:id) end def duplicate - copy = self.dup + copy = dup copy.video_data = nil copy.manuscript_data = nil copy.screenshot_data = nil @@ -38,7 +38,7 @@ def duplicate copy.parent_id = id copy.save copy.update(description: copy.description + - I18n.t('admin.question.copy_marker') + + I18n.t("admin.question.copy_marker") + copy.id.to_s) answer_map = {} answers.each { |a| answer_map[a.id] = a.duplicate(copy).id } @@ -47,19 +47,19 @@ def duplicate def self.create_prefilled(label, teachable, editors) solution = Solution.new(MampfExpression.trivial_instance) - question = Question.new(sort: 'Question', + question = Question.new(sort: "Question", description: label, teachable: teachable, editors: editors, - text: I18n.t('admin.question.initial_text'), + text: I18n.t("admin.question.initial_text"), level: 1, independent: false, - question_sort: 'mc', + question_sort: "mc", solution: solution) return question if question.invalid? Answer.create(question: question, - text: '0', + text: "0", value: true) question end @@ -74,18 +74,18 @@ def delete_vertices vertices = quiz.quiz_graph.find_vertices(self) vertices.each do |v| quiz.update(quiz_graph: quiz.quiz_graph.destroy_vertex(v), - released: 'locked') + released: "locked") end end true end def multiple_choice? - question_sort == 'mc' + question_sort == "mc" end def free_answer? - question_sort == 'free' + question_sort == "free" end def parametrized? @@ -98,10 +98,10 @@ def parsed_text_with_params end def text_with_sample_params(parameters) - return text unless parameters.present? + return text if parameters.blank? result = text - parameters.keys.each do |p| + parameters.each_key do |p| result.gsub!(/\\para{#{Regexp.escape(p)},(.*?)}/, parameters[p].to_s) end result @@ -112,13 +112,14 @@ def parameters end def sample_parameters - parameters.inject({}) { |h, (k, v)| h[k] = v.to_a.sample; h } + parameters.transform_values do |v| + v.to_a.sample + end end def self.parameters_from_text(text) text.scan(/\\para{(\w+),(.*?)}/) - .map { |v| [v[0], v[1].to_a_or_range] } - .to_h + .to_h { |v| [v[0], v[1].to_a_or_range] } end private @@ -126,7 +127,7 @@ def self.parameters_from_text(text) def prelim_answer_table table = [] size = answer_ids.count - (0..2**size - 1).each do |i| + (0..(2**size) - 1).each do |i| hash = {} i.to_bool_a(size).each_with_index.map { |x, j| hash[answer_ids[j]] = x } table.push(hash) diff --git a/app/models/question_sampler.rb b/app/models/question_sampler.rb index 2a7dacf96..79b589401 100644 --- a/app/models/question_sampler.rb +++ b/app/models/question_sampler.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # QuestionSampler class # This is a service PORO model that is used in the generation of quizzes class QuestionSampler diff --git a/app/models/quiz.rb b/app/models/quiz.rb index bd7871991..4d03afe8c 100644 --- a/app/models/quiz.rb +++ b/app/models/quiz.rb @@ -5,7 +5,7 @@ def self.new_prefilled default_table: {}, hide_solution: [])) end - def self.create_prefilled(label) + def self.create_prefilled(_label) Quiz.create(level: 1, quiz_graph: QuizGraph.new(vertices: {}, edges: {}, root: 0, default_table: {}, hide_solution: [])) @@ -18,13 +18,13 @@ def label def publish_vertices!(user, release_state) return unless vertices - vertices.keys.each do |v| + vertices.each_key do |v| quizzable = quizzable(v) next if quizzable.published? - next if !quizzable.teachable.published? + next unless quizzable.teachable.published? next unless user.in?(quizzable.editors_with_inheritance) || user.admin - quizzable.update(released: release_state, released_at: Time.now) + quizzable.update(released: release_state, released_at: Time.zone.now) end end @@ -33,7 +33,7 @@ def quizzables end def quizzables_free? - quizzables.where(released: 'all').count == quizzables.count + quizzables.where(released: "all").count == quizzables.count end def quizzables_visible_for_user?(user) @@ -41,7 +41,7 @@ def quizzables_visible_for_user?(user) end def next_vertex(progress, input = {}) - return default_table[progress] if vertices[progress][:type] == 'Remark' + return default_table[progress] if vertices[progress][:type] == "Remark" target_vertex(progress, input) end @@ -78,31 +78,29 @@ def default_table def question_ids return [] unless quiz_graph && quiz_graph.vertices.present? - quiz_graph.vertices.select { |_k, v| v[:type] == 'Question' } - .values.map { |v| v[:id] }.uniq + quiz_graph.vertices.select { |_k, v| v[:type] == "Question" } + .values.pluck(:id).uniq end def remark_ids return [] unless quiz_graph && quiz_graph.vertices.present? - quiz_graph.vertices.select { |_k, v| v[:type] == 'Remark' }.values - .map { |v| v[:id] }.uniq + quiz_graph.vertices.select { |_k, v| v[:type] == "Remark" }.values + .pluck(:id).uniq end def crosses_to_input(vertex_id, crosses) vertex = vertices[vertex_id] input = {} - if vertex[:type] == 'Question' + if vertex[:type] == "Question" question = Question.find(vertex[:id]) crosses = crosses.map(&:to_i) - input = question.answers.map { |a| [a.id, crosses.include?(a.id)] }.to_h + input = question.answers.to_h { |a| [a.id, crosses.include?(a.id)] } end input end - def quizzable(vertex_id) - quiz_graph.quizzable(vertex_id) - end + delegate :quizzable, to: :quiz_graph def preselected_branch(vertex_id, crosses, fallback) successor = next_vertex(vertex_id, crosses) @@ -116,7 +114,7 @@ def preselected_hide_solution(vertex_id, crosses) end def questions - ids = quiz_graph&.vertices&.values&.select { |v| v[:type] == 'Question' } + ids = quiz_graph&.vertices&.values&.select { |v| v[:type] == "Question" } &.map { |v| v[:id] } Question.where(id: ids) end @@ -137,6 +135,6 @@ def find_errors def target_vertex(progress, input) edges.select { |e, t| e[0] == progress && t.include?(input) }&.keys - &.first&.second || default_table[progress] + &.first&.second || default_table[progress] end end diff --git a/app/models/quiz_certificate.rb b/app/models/quiz_certificate.rb index d08069209..80c9fa8ed 100644 --- a/app/models/quiz_certificate.rb +++ b/app/models/quiz_certificate.rb @@ -1,5 +1,5 @@ class QuizCertificate < ApplicationRecord - belongs_to :quiz, class_name: 'Medium', foreign_key: 'quiz_id' + belongs_to :quiz, class_name: "Medium", inverse_of: :quiz_certificates belongs_to :user, optional: true before_create :set_code diff --git a/app/models/quiz_graph.rb b/app/models/quiz_graph.rb index 4688aa013..cb2b61033 100644 --- a/app/models/quiz_graph.rb +++ b/app/models/quiz_graph.rb @@ -17,7 +17,7 @@ def self.dump(quiz_graph) end def update_vertex(vertex_id, branching, hide) - return if @vertices[vertex_id][:type] == 'Remark' + return if @vertices[vertex_id][:type] == "Remark" remove_edges_from!(vertex_id) update_edges_for_question!(vertex_id, branching) @@ -49,9 +49,7 @@ def update_default_target!(source, target) end def delete_edge!(source, target) - if @default_table[source] == target - @default_table[source] = 0 - end + @default_table[source] = 0 if @default_table[source] == target answers = @edges[[source, target]] return unless answers @@ -70,11 +68,11 @@ def delete_edge!(source, target) # by COPIES. def replace_reference!(old_quizzable, new_quizzable, answer_map = {}) - return self unless old_quizzable.class == new_quizzable.class + return self unless old_quizzable.instance_of?(new_quizzable.class) affected_vertices = referencing_vertices(old_quizzable) affected_vertices.each { |v| @vertices[v][:id] = new_quizzable.id } - return self unless new_quizzable.class.to_s == 'Question' + return self unless new_quizzable.instance_of?(::Question) affected_vertices.each do |v| bend_edges_rereferencing!(edges_from(v), answer_map) @@ -90,28 +88,28 @@ def reset_vertex_answers_change(id) end def find_errors - return [I18n.t('admin.quiz.no_vertices')] unless @vertices.present? + return [I18n.t("admin.quiz.no_vertices")] if @vertices.blank? - branch_undef = @default_table.values.include?(0) - no_end = default_table.values.exclude?(-1) && @edges.select { |e| + branch_undef = @default_table.value?(0) + no_end = default_table.values.exclude?(-1) && @edges.select do |e| e[1] == -1 - }.blank? + end.blank? no_root = @root.blank? || @root.zero? messages = [] - messages.push(I18n.t('admin.quiz.undefined_targets')) if branch_undef - messages.push(I18n.t('admin.quiz.no_end')) if no_end - messages.push(I18n.t('admin.quiz.no_start')) if no_root + messages.push(I18n.t("admin.quiz.undefined_targets")) if branch_undef + messages.push(I18n.t("admin.quiz.no_end")) if no_end + messages.push(I18n.t("admin.quiz.no_start")) if no_root messages end def warnings - return I18n.t('admin.quiz.unreleased_vertices') if unreleased_vertices? + I18n.t("admin.quiz.unreleased_vertices") if unreleased_vertices? end def quizzable(id) return unless id.in?(@vertices.keys) - @vertices[id][:type].constantize.find_by_id(@vertices[id][:id]) + @vertices[id][:type].constantize.find_by(id: @vertices[id][:id]) end def visible?(id) @@ -160,7 +158,7 @@ def remove_edges_from!(vertex_id) def update_edges_for_question!(vertex_id, branching) new_hash = Hash.new { |h, k| h[k] = [] } - new_edges = branching.each_with_object(new_hash) { |(k, v), h| h[v] << k } + branching.each_with_object(new_hash) { |(k, v), h| h[v] << k } default_edge = [vertex_id, @default_table[vertex_id]] @edges.merge!(new_hash.except(default_edge)) end @@ -196,11 +194,11 @@ def edges_to(id) end def incoming(id) - edges_to(id).map { |e| e[0] } + edges_to(id).pluck(0) end def neighbours(id) - edges_from_plus_default(id).map { |e| e[1] } + edges_from_plus_default(id).pluck(1) end def referencing_vertices(quizzable) @@ -211,21 +209,21 @@ def referencing_vertices(quizzable) end def new_vertex_id - return 1 unless @vertices.present? + return 1 if @vertices.blank? @vertices.keys.max + 1 end def edge_color_for_cytoscape(edge) - @default_table[edge[0]] == edge[1] ? '#32cd32' : '#f00' + @default_table[edge[0]] == edge[1] ? "#32cd32" : "#f00" end def border_color_for_cytoscape(id) quizzable = quizzable(id) - return 'orange' unless quizzable.visible? - return 'chocolate' if quizzable.restricted? + return "orange" unless quizzable.visible? + return "chocolate" if quizzable.restricted? - '#222' + "#222" end def linearize! @@ -248,8 +246,8 @@ def self.build_from_questions(question_ids) question_ids.each_with_index do |q, i| j = i + 1 k = j < size ? j + 1 : -1 - question = Question.find_by_id(q) - vertices[j] = { type: 'Question', id: q } + Question.find_by(id: q) + vertices[j] = { type: "Question", id: q } default_table[j] = k end QuizGraph.new(vertices: vertices, edges: edges, root: 1, @@ -258,32 +256,32 @@ def self.build_from_questions(question_ids) def to_cytoscape result = [] - result.push(data: { id: '-2', - label: I18n.t('admin.quiz.start'), - color: '#000', - background: 'yellowgreen', - borderwidth: '0', - bordercolor: 'grey', - shape: 'diamond' }) + result.push(data: { id: "-2", + label: I18n.t("admin.quiz.start"), + color: "#000", + background: "yellowgreen", + borderwidth: "0", + bordercolor: "grey", + shape: "diamond" }) # add vertices - @vertices.keys.each do |v| + @vertices.each_key do |v| result.push(data: cytoscape_vertex(v)) end - result.push(data: { id: '-1', - label: I18n.t('admin.quiz.end'), - color: '#000', - background: 'yellowgreen', - borderwidth: '0', - bordercolor: '#f4a460', - shape: 'diamond' }) + result.push(data: { id: "-1", + label: I18n.t("admin.quiz.end"), + color: "#000", + background: "yellowgreen", + borderwidth: "0", + bordercolor: "#f4a460", + shape: "diamond" }) # add edges if @root.in?(@vertices.keys) result.push(data: { id: "-2-#{@root}", source: -2, target: @root, - color: '#aaa' }) + color: "#aaa" }) end - @vertices.keys.each do |v| + @vertices.each_key do |v| edges_from_plus_default(v).each do |e| result.push(data: cytoscape_edge(e)) end @@ -299,11 +297,11 @@ def linear? def cytoscape_vertex(id) { id: id.to_s, label: quizzable(id).description, - color: '#000', - background: @vertices[id][:type] == 'Question' ? '#e1f5fe' : '#f9fbe7', - borderwidth: '2', + color: "#000", + background: @vertices[id][:type] == "Question" ? "#e1f5fe" : "#f9fbe7", + borderwidth: "2", bordercolor: border_color_for_cytoscape(id), - shape: @vertices[id][:type] == 'Question' ? 'ellipse' : 'rectangle', + shape: @vertices[id][:type] == "Question" ? "ellipse" : "rectangle", defaulttarget: @default_table[id] } end @@ -317,7 +315,7 @@ def cytoscape_edge(edge) end def questions_count - @vertices.values.select { |v| v[:type] == 'Question' }.count + @vertices.values.count { |v| v[:type] == "Question" } end def default?(edge) diff --git a/app/models/quiz_round.rb b/app/models/quiz_round.rb index 3cfdd9c81..eb53a1d0e 100644 --- a/app/models/quiz_round.rb +++ b/app/models/quiz_round.rb @@ -20,8 +20,8 @@ def initialize(params) progress_counter(params) @vertex = @quiz.vertices[@progress] @vertex_old = @vertex - question_details(params) if @vertex.present? && @vertex[:type] == 'Question' - remark_details(params) if @vertex.present? && @vertex[:type] == 'Remark' + question_details(params) if @vertex.present? && @vertex[:type] == "Question" + remark_details(params) if @vertex.present? && @vertex[:type] == "Remark" @answer_scheme ||= {} @answer_shuffle ||= [] @answer_shuffle_old = [] @@ -35,47 +35,47 @@ def update create_question_probe if @is_question create_remark_probe if @is_remark && @study_participant @progress = @quiz.next_vertex(@progress, @input) - create_certificate_final_probe if @progress == -1 && @quiz.sort == 'Quiz' + create_certificate_final_probe if @progress == -1 && @quiz.sort == "Quiz" @counter += 1 @hide_solution = @quiz.quiz_graph.hide_solution .include?([@progress_old, @input]) @vertex = @quiz.vertices[@progress] @answer_shuffle_old = @answer_shuffle - update_answer_shuffle if @vertex && @vertex[:type] == 'Question' + update_answer_shuffle if @vertex && @vertex[:type] == "Question" self end def round_id - 'round' + @progress.to_s + '-' + @counter.to_s + "round#{@progress}-#{@counter}" end def background - return 'bg-grey-lighten-4' if @hide_solution - return 'bg-correct' if @correct + return "bg-grey-lighten-4" if @hide_solution + return "bg-correct" if @correct - 'bg-incorrect' + "bg-incorrect" end def badge - 'badge bg-' + (@correct ? 'success' : 'danger') + "badge bg-#{@correct ? "success" : "danger"}" end def statement - return I18n.t('admin.quiz.correct_result') if @correct + return I18n.t("admin.quiz.correct_result") if @correct - I18n.t('admin.quiz.incorrect_result') + I18n.t("admin.quiz.incorrect_result") end def answers return [] unless @answer_shuffle - @answer_shuffle.map { |a| Answer.find_by_id(a) } + @answer_shuffle.map { |a| Answer.find_by(id: a) } end def answers_old return [] unless @answer_shuffle_old - @answer_shuffle_old.map { |a| Answer.find_by_id(a) } + @answer_shuffle_old.map { |a| Answer.find_by(id: a) } end private @@ -88,7 +88,7 @@ def progress_counter(params) end @progress ||= @quiz.root @counter ||= 0 - @session_id ||= SecureRandom.uuid.first(13).remove('-') + @session_id ||= SecureRandom.uuid.first(13).remove("-") @progress_old = @progress @counter_old = @counter @round_id_old = round_id @@ -98,27 +98,27 @@ def question_details(params) @is_question = true @question_id = @vertex[:id] @answer_scheme = Question.find(@question_id).answer_scheme - if params[:quiz].present? && params[:quiz][:answer_shuffle].present? - @answer_shuffle = JSON.parse(params[:quiz][:answer_shuffle]).map(&:to_i) + @answer_shuffle = if params[:quiz].present? && params[:quiz][:answer_shuffle].present? + JSON.parse(params[:quiz][:answer_shuffle]).map(&:to_i) else - @answer_shuffle = Question.find(@question_id).answers.map(&:id).shuffle + Question.find(@question_id).answers.map(&:id).shuffle end end - def remark_details(params) + def remark_details(_params) @is_remark = true @remark_id = @vertex[:id] end def update_answer_shuffle - @answer_shuffle = Question.find_by_id(@vertex[:id])&.answers&.map(&:id) - &.shuffle + @answer_shuffle = Question.find_by(id: @vertex[:id])&.answers&.map(&:id) + &.shuffle end def create_question_probe return unless @save_probe - quiz_id = @quiz.id unless @quiz.sort == 'RandomQuiz' + quiz_id = @quiz.id unless @quiz.sort == "RandomQuiz" input = @solution_input || @input.to_s if @study_participant ProbeSaver.perform_async(quiz_id, @question_id, nil, @correct, @progress, @session_id, @study_participant, input) @@ -127,7 +127,7 @@ def create_question_probe def create_remark_probe return unless @save_probe - quiz_id = @quiz.id unless @quiz.sort == 'RandomQuiz' + quiz_id = @quiz.id unless @quiz.sort == "RandomQuiz" ProbeSaver.perform_async(quiz_id, nil, @remark_id, nil, @progress, @session_id, @study_participant, @input_text) end diff --git a/app/models/reader.rb b/app/models/reader.rb index 0ba906da7..5fd10cab3 100644 --- a/app/models/reader.rb +++ b/app/models/reader.rb @@ -3,5 +3,5 @@ # making it possible to display whether there are new comments class Reader < ApplicationRecord belongs_to :user - belongs_to :thread, class_name: 'Commontator::Thread' + belongs_to :thread, class_name: "Commontator::Thread" end diff --git a/app/models/referral.rb b/app/models/referral.rb index ecf3e6f32..769d0a116 100644 --- a/app/models/referral.rb +++ b/app/models/referral.rb @@ -28,32 +28,30 @@ def explain # provide time span for vtt file def vtt_time_span - start_time.vtt_string + ' --> ' + end_time.vtt_string + "\n" + "#{start_time.vtt_string} --> #{end_time.vtt_string}\n" end # provide metadata for vtt file def vtt_properties - link = item.link.present? ? item.link : item.medium_link + link = (item.link.presence || item.medium_link) # at the moment, relations between items can be only of the form # script <-> video, which means that between them there will be at most # one script, one manuscript and one video - if item.medium&.sort == 'Script' + if item.medium&.sort == "Script" script = item.manuscript_link if item.related_items_visible? video = item.related_items&.first&.video_link manuscript = item.related_items&.first&.manuscript_link end else - if item.related_items_visible? - script = item.related_items&.first&.manuscript_link - end + script = item.related_items&.first&.manuscript_link if item.related_items_visible? manuscript = item.manuscript_link video = item.video_link end - { 'video' => video, 'manuscript' => manuscript, - 'script' => script, 'link' => link, 'quiz' => item.quiz_link, - 'reference' => item.vtt_meta_reference(medium), - 'text' => item.vtt_text, 'explanation' => vtt_explanation }.compact + { "video" => video, "manuscript" => manuscript, + "script" => script, "link" => link, "quiz" => item.quiz_link, + "reference" => item.vtt_meta_reference(medium), + "text" => item.vtt_text, "explanation" => vtt_explanation }.compact end # returns whether this referral's item has been referred to @@ -65,18 +63,18 @@ def reappears # initial description in the referral form def prefilled_description - item.present? ? item.description : '' + item.present? ? item.description : "" end # initial link in the referral form def prefilled_link - item.present? ? item.link : '' + item.present? ? item.link : "" end # returns true iff the referral's item's medium has an associated video, but # the item is not a pdf destination def video? - !!item&.video? && item.sort != 'pdf_destination' + !!item&.video? && item.sort != "pdf_destination" end def manuscript? @@ -110,7 +108,8 @@ def item_in_quarantine? # if the item is a link, otherwise nil def vtt_explanation return explanation if explanation.present? - return item.explanation if item.sort == 'link' && item.explanation.present? + + item.explanation if item.sort == "link" && item.explanation.present? end # some method that check for valid start and end time diff --git a/app/models/relation.rb b/app/models/relation.rb index a3775e48e..91f387882 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -2,14 +2,14 @@ # Join table for tag<->tag many-to-many-relation class Relation < ApplicationRecord belongs_to :tag - belongs_to :related_tag, class_name: 'Tag' + belongs_to :related_tag, class_name: "Tag" validates :related_tag, uniqueness: { scope: :tag } + before_destroy :touch_tag + after_destroy :destroy_inverses, if: :inverse? after_save :create_inverse, unless: :inverse? after_save :destroy, if: :self_inverse? after_save :touch_tag - before_destroy :touch_tag - after_destroy :destroy_inverses, if: :inverse? private diff --git a/app/models/remark.rb b/app/models/remark.rb index 2e9309d46..5f741477a 100644 --- a/app/models/remark.rb +++ b/app/models/remark.rb @@ -14,17 +14,17 @@ def quiz_ids end def self.create_prefilled(label, teachable, editors) - remark = Remark.new(sort: 'Remark', + remark = Remark.new(sort: "Remark", description: label, teachable: teachable, editors: editors, - text: I18n.t('admin.remark.initial_text')) + text: I18n.t("admin.remark.initial_text")) remark.save remark end def duplicate - copy = self.dup + copy = dup copy.video_data = nil copy.manuscript_data = nil copy.screenshot_data = nil @@ -32,7 +32,7 @@ def duplicate copy.parent_id = id copy.save copy.update(description: copy.description + - I18n.t('admin.remark.copy_marker') + copy.id.to_s) + I18n.t("admin.remark.copy_marker") + copy.id.to_s) copy end @@ -44,10 +44,10 @@ def delete_vertices quiz_ids.each do |q| quiz = Quiz.find(q) vertices = quiz.vertices - .select { |_k, v| v == { type: 'Remark', id: id } }.keys + .select { |_k, v| v == { type: "Remark", id: id } }.keys vertices.each do |v| quiz.update(quiz_graph: quiz.quiz_graph.destroy_vertex(v), - released: 'locked') + released: "locked") end end end diff --git a/app/models/section.rb b/app/models/section.rb index e6287dcfe..9c54309b4 100644 --- a/app/models/section.rb +++ b/app/models/section.rb @@ -23,6 +23,9 @@ class Section < ApplicationRecord # a section has many items, do not execute callbacks when section is destroyed has_many :items, dependent: :nullify + before_destroy :touch_toc + before_destroy :touch_lecture + before_destroy :touch_media # after saving or updating, touch lecture/media/self to keep cache up to date after_save :touch_lecture after_save :touch_media @@ -31,23 +34,19 @@ class Section < ApplicationRecord # if absolute numbering is enabled for the lecture, all chapters # and sections need to be touched because of possibly changed references after_save :touch_toc - before_destroy :touch_toc - - before_destroy :touch_lecture - before_destroy :touch_media def lecture chapter&.lecture end def reference_number - return calculated_number unless display_number.present? + return calculated_number if display_number.blank? display_number end def displayed_number - '§' + reference_number + "§#{reference_number}" end def reference @@ -61,15 +60,15 @@ def reference # chapters def calculated_number return relative_position unless lecture.absolute_numbering - return absolute_position.to_s unless lecture.start_section.present? + return absolute_position.to_s if lecture.start_section.blank? (absolute_position + lecture.start_section - 1).to_s end def to_label - return displayed_number + '. ' + title unless hidden_with_inheritance? + return "#{displayed_number}. #{title}" unless hidden_with_inheritance? - '*' + displayed_number + '. ' + title + "*#{displayed_number}. #{title}" end # section's media are media that are contained in one of the @@ -152,12 +151,12 @@ def script_items_by_position end def visible_items_by_time - lessons.order(:date).map { |l| l.visible_items }.flatten + lessons.order(:date).map(&:visible_items).flatten .select { |i| i.section == self } end def visible_items - return visible_items_by_time if lecture.content_mode == 'video' + return visible_items_by_time if lecture.content_mode == "video" script_items_by_position end @@ -167,7 +166,7 @@ def hidden_with_inheritance? end def cache_key - super + '-' + I18n.locale.to_s + "#{super}-#{I18n.locale}" end def duplicate_in_chapter(new_chapter, import_tags) @@ -189,7 +188,7 @@ def touch_lecture end def touch_media - lecture.media_with_inheritance.update_all(updated_at: Time.current) + lecture.media_with_inheritance.touch_all touch end @@ -200,12 +199,12 @@ def touch_self def touch_toc return unless lecture.absolute_numbering - lecture.chapters.update_all(updated_at: Time.now) - lecture.sections.update_all(updated_at: Time.now) + lecture.chapters.touch_all + lecture.sections.touch_all end def relative_position - chapter.displayed_number + '.' + position.to_s + "#{chapter.displayed_number}.#{position}" end def absolute_position diff --git a/app/models/solution.rb b/app/models/solution.rb index 010ca8f2d..5a24a4d0e 100644 --- a/app/models/solution.rb +++ b/app/models/solution.rb @@ -32,20 +32,20 @@ def nerd end def tex - return '' unless @content.tex + return "" unless @content.tex - '$$' + @content.tex + '$$' + "$$#{@content.tex}$$" end def tex_mc_answer - return '' unless @content.tex + return "" unless @content.tex - '$' + @content.tex + '$' + "$#{@content.tex}$" end def self.from_hash(solution_type, content) - return unless solution_type.in?(['MampfExpression', 'MampfMatrix', - 'MampfTuple', 'MampfSet']) + return unless solution_type.in?(["MampfExpression", "MampfMatrix", + "MampfTuple", "MampfSet"]) solution = Solution.new(solution_type.constantize.from_hash(content)) solution.explanation = content[:explanation] diff --git a/app/models/speaker_talk_join.rb b/app/models/speaker_talk_join.rb index 275b0aa0f..2248ae5d4 100644 --- a/app/models/speaker_talk_join.rb +++ b/app/models/speaker_talk_join.rb @@ -1,4 +1,4 @@ class SpeakerTalkJoin < ApplicationRecord belongs_to :talk - belongs_to :speaker, class_name: 'User', foreign_key: 'speaker_id' + belongs_to :speaker, class_name: "User", inverse_of: :speaker_talk_joins end diff --git a/app/models/submission.rb b/app/models/submission.rb index 8b352da41..61c640286 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -23,43 +23,43 @@ def partners_of_user(user) end def team - users.map(&:tutorial_name).natural_sort.join(', ') + users.map(&:tutorial_name).natural_sort.join(", ") end def manuscript_filename - return unless manuscript.present? + return if manuscript.blank? - manuscript.metadata['filename'] + manuscript.metadata["filename"] end def manuscript_size - return unless manuscript.present? + return if manuscript.blank? - manuscript.metadata['size'] + manuscript.metadata["size"] end def manuscript_mime_type - return unless manuscript.present? + return if manuscript.blank? - manuscript.metadata['mime_type'] + manuscript.metadata["mime_type"] end def correction_filename - return unless correction.present? + return if correction.blank? - correction.metadata['filename'] + correction.metadata["filename"] end def correction_mime_type - return unless correction.present? + return if correction.blank? - correction.metadata['mime_type'] + correction.metadata["mime_type"] end def correction_size - return unless correction.present? + return if correction.blank? - correction.metadata['size'] + correction.metadata["size"] end def preceding_tutorial(user) @@ -105,28 +105,29 @@ def not_updatable? # correction.to_io.path # end - def filename_for_bulk_download(end_of_file = '') - (team.first(180) + '-' + - last_modification_by_users_at.strftime("%F-%H%M") + - (too_late? ? '-LATE' : '') + - + '-ID-' + id + end_of_file + - assignment.accepted_file_type) - .gsub(/[\x00\/\\:\*\?\"<>\|]/, '_') - .gsub(/^.*(\\|\/)/, '') + def filename_for_bulk_download(end_of_file = "") + start_str = "#{team.first(180)}-#{last_modification_by_users_at.strftime("%F-%H%M")}" + too_late_str = too_late? ? "-LATE" : "" + id_str = "-ID-#{id}" + end_of_file_str = "#{end_of_file}#{assignment.accepted_file_type}" + + "#{start_str}#{too_late_str}#{id_str}#{end_of_file_str}" + .gsub(%r{[\x00/\\:\*\?\"<>\|]}, "_") + .gsub(%r{^.*(\\|/)}, "") # Strip out the non-ascii characters - .gsub(/[^0-9A-Za-z.\-]/, '_') + .gsub(/[^0-9A-Za-z.\-]/, "_") end - def self.zip(submissions, downloadables, end_of_file = '') + def self.zip(submissions, downloadables, end_of_file = "") begin archived_filestream = Zip::OutputStream.write_buffer do |stream| submissions.zip(downloadables).each do |s, d| stream.put_next_entry(s.filename_for_bulk_download(end_of_file)) - stream.write IO.read(d.to_io.path) + stream.write(File.read(d.to_io.path)) end end archived_filestream.rewind - rescue => e + rescue StandardError => e archived_filestream = e.message end archived_filestream @@ -135,20 +136,20 @@ def self.zip(submissions, downloadables, end_of_file = '') def self.zip_submissions!(tutorial, assignment) submissions = Submission.where(tutorial: tutorial, assignment: assignment).proper - manuscripts = submissions.collect { |s| - s.manuscript if s.manuscript.present? - } + manuscripts = submissions.collect do |s| + s.manuscript.presence + end zip(submissions, manuscripts) end def self.zip_corrections!(tutorial, assignment) submissions = Submission.where(tutorial: tutorial, assignment: assignment).proper - corrections = submissions.collect { |s| - s.correction if s.correction.present? - } + corrections = submissions.collect do |s| + s.correction.presence + end - zip(submissions, corrections, '-correction') + zip(submissions, corrections, "-correction") end ### @@ -156,69 +157,69 @@ def self.zip_corrections!(tutorial, assignment) ### def check_file_properties_any(metadata, sort) errors = [] - if sort == :submission && metadata['size'] > 10 * 1024 * 1024 - errors.push I18n.t('submission.manuscript_size_too_big', - max_size: '10 MB') + if sort == :submission && metadata["size"] > 10 * 1024 * 1024 + errors.push(I18n.t("submission.manuscript_size_too_big", + max_size: "10 MB")) end - if sort == :correction && metadata['size'] > 15 * 1024 * 1024 - errors.push I18n.t('submission.manuscript_size_too_big', - max_size: '15 MB') + if sort == :correction && metadata["size"] > 15 * 1024 * 1024 + errors.push(I18n.t("submission.manuscript_size_too_big", + max_size: "15 MB")) end - file_name = metadata['filename'] + file_name = metadata["filename"] file_type = File.extname(file_name) - if !file_type.in?(['.cc', '.hh', '.m', ".mlx", '.pdf', '.zip', ".txt"]) - errors.push I18n.t('submission.wrong_file_type', + unless file_type.in?([".cc", ".hh", ".m", ".mlx", ".pdf", ".zip", ".txt"]) + errors.push(I18n.t("submission.wrong_file_type", file_type: file_type, - accepted_file_type: assignment.accepted_file_type) + accepted_file_type: assignment.accepted_file_type)) end - return {} unless errors.present? + return {} if errors.blank? { sort => errors } end def check_file_properties(metadata, sort) errors = [] - if sort == :submission && metadata['size'] > 10 * 1024 * 1024 - errors.push I18n.t('submission.manuscript_size_too_big', - max_size: '10 MB') + if sort == :submission && metadata["size"] > 10 * 1024 * 1024 + errors.push(I18n.t("submission.manuscript_size_too_big", + max_size: "10 MB")) end - if sort == :correction && metadata['size'] > 15 * 1024 * 1024 - errors.push I18n.t('submission.manuscript_size_too_big', - max_size: '15 MB') + if sort == :correction && metadata["size"] > 15 * 1024 * 1024 + errors.push(I18n.t("submission.manuscript_size_too_big", + max_size: "15 MB")) end - file_name = metadata['filename'] + file_name = metadata["filename"] file_type = File.extname(file_name) if file_type != assignment.accepted_file_type && - assignment.accepted_file_type != '.tar.gz' - errors.push I18n.t('submission.wrong_file_type', + assignment.accepted_file_type != ".tar.gz" + errors.push(I18n.t("submission.wrong_file_type", file_type: file_type, - accepted_file_type: assignment.accepted_file_type) + accepted_file_type: assignment.accepted_file_type)) end - if assignment.accepted_file_type == '.tar.gz' - if file_type == '.gz' - reduced_type = File.extname(File.basename(file_name, '.gz')) - if reduced_type != '.tar' - errors.push I18n.t('submission.wrong_file_type', - file_type: '.gz', - accepted_file_type: '.tar.gz') + if assignment.accepted_file_type == ".tar.gz" + if file_type == ".gz" + reduced_type = File.extname(File.basename(file_name, ".gz")) + if reduced_type != ".tar" + errors.push(I18n.t("submission.wrong_file_type", + file_type: ".gz", + accepted_file_type: ".tar.gz")) end else - errors.push I18n.t('submission.wrong_file_type', + errors.push(I18n.t("submission.wrong_file_type", file_type: file_type, - accepted_file_type: '.tar.gz') + accepted_file_type: ".tar.gz")) end end - if (!assignment.accepted_file_type.in?(['.cc', '.hh', '.m']) && - !metadata['mime_type'].in?(assignment.accepted_mime_types)) || - (assignment.accepted_file_type.in?(['.cc', '.hh', '.m']) && - (!metadata['mime_type'].starts_with?('text/') && - metadata['mime_type'] != 'application/octet-stream')) - errors.push I18n.t('submission.wrong_mime_type', - mime_type: metadata['mime_type'], + if (!assignment.accepted_file_type.in?([".cc", ".hh", ".m"]) && + !metadata["mime_type"].in?(assignment.accepted_mime_types)) || + (assignment.accepted_file_type.in?([".cc", ".hh", ".m"]) && + (!metadata["mime_type"].starts_with?("text/") && + metadata["mime_type"] != "application/octet-stream")) + errors.push(I18n.t("submission.wrong_mime_type", + mime_type: metadata["mime_type"], accepted_mime_types: assignment.accepted_mime_types - .join(', ')) + .join(", "))) end - return {} unless errors.present? + return {} if errors.blank? { sort => errors } end @@ -229,18 +230,18 @@ def self.bulk_corrections!(tutorial, assignment, files) report = { successful_saves: [], submissions: submissions.size, invalid_filenames: [], invalid_id: [], in_subfolder: [], no_decision: [], rejected: [], invalid_file: [] } - tmp_folder = Dir.mktmpdir + Dir.mktmpdir begin files.each do |file_shrine| filename = file_shrine["metadata"]["filename"] - if !'-ID-'.in?(filename) + unless "-ID-".in?(filename) report[:invalid_filenames].push(filename) next end - submission_id = File.basename(filename.split('-ID-').last, - File.extname(filename.split('-ID-').last)) - submission = Submission.find_by_id(submission_id) - if !submission + submission_id = File.basename(filename.split("-ID-").last, + File.extname(filename.split("-ID-").last)) + submission = Submission.find_by(id: submission_id) + unless submission report[:invalid_id].push(filename) next end @@ -253,59 +254,59 @@ def self.bulk_corrections!(tutorial, assignment, files) next end submission.update(correction: file_shrine.to_json) - if !submission.valid? + unless submission.valid? report[:invalid_file].push(filename) next end report[:successful_saves].push(submission) end - rescue => e - report[:errors] = "#{e.message}" + rescue StandardError => e + report[:errors] = e.message.to_s end report end - private + def self.number_of_submissions(tutorial, assignment) + Submission.where(tutorial: tutorial, assignment: assignment) + .where.not(manuscript_data: nil).size + end - def matching_lecture - return true if tutorial&.lecture == assignment&.lecture + def self.number_of_corrections(tutorial, assignment) + Submission.where(tutorial: tutorial, assignment: assignment) + .where.not(correction_data: nil).size + end - errors.add(:tutorial, :lecture_not_matching) - end + def self.number_of_late_submissions(tutorial, assignment) + Submission.where(tutorial: tutorial, assignment: assignment) + .where.not(manuscript_data: nil) + .count(&:too_late?) + end - def set_token - self.token = Submission.generate_token - end + def self.submissions_total(assignment) + Submission.where(assignment: assignment) + .where.not(manuscript_data: nil).size + end - def self.number_of_submissions(tutorial, assignment) - Submission.where(tutorial: tutorial, assignment: assignment) - .where.not(manuscript_data: nil).size - end + def self.corrections_total(assignment) + Submission.where(assignment: assignment) + .where.not(correction_data: nil).size + end - def self.number_of_corrections(tutorial, assignment) - Submission.where(tutorial: tutorial, assignment: assignment) - .where.not(correction_data: nil).size - end + def self.late_submissions_total(assignment) + Submission.where(assignment: assignment) + .where.not(manuscript_data: nil) + .count(&:too_late?) + end - def self.number_of_late_submissions(tutorial, assignment) - Submission.where(tutorial: tutorial, assignment: assignment) - .where.not(manuscript_data: nil) - .select { |s| s.too_late? }.size - end + private - def self.submissions_total(assignment) - Submission.where(assignment: assignment) - .where.not(manuscript_data: nil).size - end + def matching_lecture + return true if tutorial&.lecture == assignment&.lecture - def self.corrections_total(assignment) - Submission.where(assignment: assignment) - .where.not(correction_data: nil).size + errors.add(:tutorial, :lecture_not_matching) end - def self.late_submissions_total(assignment) - Submission.where(assignment: assignment) - .where.not(manuscript_data: nil) - .select { |s| s.too_late? }.size + def set_token + self.token = Submission.generate_token end end diff --git a/app/models/submission_cleaner.rb b/app/models/submission_cleaner.rb index 5a91644cc..05c416e27 100644 --- a/app/models/submission_cleaner.rb +++ b/app/models/submission_cleaner.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # PORO class that handles the cleaning of submissions # in order to fulfill GDPR regulations class SubmissionCleaner diff --git a/app/models/tag.rb b/app/models/tag.rb index 42d1499a0..c92002ec0 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -29,26 +29,30 @@ class Tag < ApplicationRecord has_many :related_tags, through: :relations, after_remove: :destroy_relations # a tag has different notions in different languages - has_many :notions, foreign_key: 'tag_id', - after_remove: :touch_relations, - after_add: :touch_relations, - dependent: :destroy - has_many :aliases, foreign_key: 'aliased_tag_id', class_name: 'Notion' + has_many :notions, + after_remove: :touch_relations, + after_add: :touch_relations, + dependent: :destroy, + inverse_of: :tag + has_many :aliases, + foreign_key: "aliased_tag_id", + class_name: "Notion", + inverse_of: :aliased_tag serialize :realizations, Array accepts_nested_attributes_for :notions, reject_if: lambda { |attributes| - attributes['title'].blank? + attributes["title"].blank? }, allow_destroy: true - validates_presence_of :notions + validates :notions, presence: true validates_associated :notions accepts_nested_attributes_for :aliases, reject_if: lambda { |attributes| - attributes['title'].blank? + attributes["title"].blank? }, allow_destroy: true @@ -79,19 +83,15 @@ def title end def extended_title_uncached - unless other_titles_uncached.any? || aliases.any? - return local_title_uncached - end - unless aliases.any? - return local_title_uncached + " (#{other_titles_uncached.join(', ')})" - end + return local_title_uncached unless other_titles_uncached.any? || aliases.any? + return local_title_uncached + " (#{other_titles_uncached.join(", ")})" unless aliases.any? unless other_titles_uncached.any? - return local_title_uncached + " (#{aliases.pluck(:title).join(', ')})" + return local_title_uncached + " (#{aliases.pluck(:title).join(", ")})" end local_title_uncached + - " (#{aliases.pluck(:title).join(', ')}," + - " #{other_titles_uncached.join(', ')})" + " (#{aliases.pluck(:title).join(", ")}, " \ + "#{other_titles_uncached.join(", ")})" end def extended_title @@ -133,7 +133,7 @@ def extended_title_id_hash end def locale_title_hash - notions.map { |n| [n.locale, n.title] }.to_h + notions.to_h { |n| [n.locale, n.title] } end def self.select_with_substring(search_string) @@ -142,11 +142,11 @@ def self.select_with_substring(search_string) search = Sunspot.new_search(Tag) search.build do - fulltext search_string + fulltext(search_string) end search.execute - result = search.results - .map { |t| { value: t.id, text: t.title } } + search.results + .map { |t| { value: t.id, text: t.title } } end # returns all tags whose title is close to the given search string @@ -169,7 +169,7 @@ def self.select_by_title_cached # returns the array of all tags (sorted by title) together with # their ids def self.select_by_title - Tag.all.map { |t| t.extended_title_id_hash } + Tag.all.map(&:extended_title_id_hash) .natural_sort_by { |t| t[:title] }.map { |t| [t[:title], t[:id]] } end @@ -177,7 +177,7 @@ def self.select_by_title # arel of tags together with def self.select_by_title_except(excluded_tags) Tag.where.not(id: excluded_tags.pluck(:id)) - .map { |t| t.extended_title_id_hash } + .map(&:extended_title_id_hash) .natural_sort_by { |t| t[:title] }.map { |t| [t[:title], t[:id]] } end @@ -211,7 +211,7 @@ def realizations_cached # returns the ARel of all tags or whose id is among a given array of ids # search params is a hash having keys :all_tags, :tag_ids def self.search_tags(search_params) - return Tag.all unless search_params[:all_tags] == '0' + return Tag.all unless search_params[:all_tags] == "0" tag_ids = search_params[:tag_ids] || [] Tag.where(id: tag_ids) @@ -244,7 +244,7 @@ def tags_in_neighbourhood def short_title(max_letters = 30) return title unless title.length > max_letters - title[0, max_letters - 3] + '...' + "#{title[0, max_letters - 3]}..." end def in_lecture?(lecture) @@ -274,10 +274,12 @@ def create_random_quiz!(user) question_ids = questions.pluck(:id).sample(5) quiz_graph = QuizGraph.build_from_questions(question_ids) - quiz = Quiz.new(description: "#{I18n.t('categories.randomquiz.singular')} #{title} #{Time.now}", + + quiz_i18n = I18n.t("categories.randomquiz.singular") + quiz = Quiz.new(description: "#{quiz_i18n} #{title} #{Time.zone.now}", level: 1, quiz_graph: quiz_graph, - sort: 'RandomQuiz') + sort: "RandomQuiz") quiz.save return quiz.errors unless quiz.valid? @@ -287,19 +289,19 @@ def create_random_quiz!(user) # returns the vertex title color of the tag in the neighbourhood graph of # the given marked tag def color(marked_tag, highlight_related_tags: true) - return '#f00' if self == marked_tag - return '#ff8c00' if highlight_related_tags && in?(marked_tag.related_tags) + return "#f00" if self == marked_tag + return "#ff8c00" if highlight_related_tags && in?(marked_tag.related_tags) - '#000' + "#000" end # returns the vertex color of the tag in the neighbourhood graph of # the given marked tag def background(marked_tag, highlight_related_tags: true) - return '#f00' if self == marked_tag - return '#ff8c00' if highlight_related_tags && in?(marked_tag.related_tags) + return "#f00" if self == marked_tag + return "#ff8c00" if highlight_related_tags && in?(marked_tag.related_tags) - '#666' + "#666" end # returns the cytoscape hash describing the tag's vertex in the neighbourhood @@ -323,27 +325,25 @@ def cytoscape_edge(related_tag) # published sections are sections that belong to a published lecture def visible_sections(user) - user.filter_sections(sections).select { |s| + user.filter_sections(sections).select do |s| s.lecture.visible_for_user?(user) - } + end end def cache_key - super + '-' + I18n.locale.to_s + "#{super}-#{I18n.locale}" end def touch_lectures - Lecture.where(id: sections.map(&:lecture) - .map(&:id)).update_all updated_at: Time.now + Lecture.where(id: sections.map { |section| section.lecture.id }).touch_all end def touch_sections - sections.update_all updated_at: Time.now + sections.touch_all end def touch_chapters - Chapter.where(id: sections.map(&:chapter) - .map(&:id)).update_all updated_at: Time.now + Chapter.where(id: sections.map { |section| section.chapter.id }).touch_all end def identify_with!(tag) @@ -354,7 +354,7 @@ def identify_with!(tag) related_tags << (tag.related_tags - related_tags) related_tags.delete(tag) tag.sections.each do |s| - next unless self.in?(s.tags) + next unless in?(s.tags) old_section_tag = SectionTagJoin.find_by(section: s, tag: tag) position = old_section_tag.tag_position @@ -362,7 +362,7 @@ def identify_with!(tag) new_section_tag.insert_at(position) old_section_tag.move_to_bottom end - tag.aliases.update_all(aliased_tag_id: id) + tag.aliases.update(aliased_tag_id: id) end def common_titles(tag) @@ -371,24 +371,24 @@ def common_titles(tag) result[l] = [locale_title_hash[l.to_s]] + [tag.locale_title_hash[l.to_s]] result[l].delete(nil) result[:contradictions].push(l) if result[l].count > 1 - result.delete(l) unless result[l].present? + result.delete(l) if result[l].blank? end result end def visible_questions(user) - user.filter_visible_media(user.filter_media(media.where(type: 'Question'))) + user.filter_visible_media(user.filter_media(media.where(type: "Question"))) end private - def touch_relations(notion) - if persisted? - touch - touch_lectures - touch_sections - touch_chapters - end + def touch_relations(_notion) + return unless persisted? + + touch + touch_lectures + touch_sections + touch_chapters end # simulates the after_destroy callback for relations @@ -398,9 +398,9 @@ def destroy_relations(related_tag) end def title_join - result = notions.pluck(:title).join(' ') + result = notions.pluck(:title).join(" ") return result unless aliases.any? - result + ' ' + aliases.pluck(:title).join(' ') + "#{result} #{aliases.pluck(:title).join(" ")}" end end diff --git a/app/models/talk.rb b/app/models/talk.rb index 5b528e139..2afef84a1 100644 --- a/app/models/talk.rb +++ b/app/models/talk.rb @@ -8,13 +8,14 @@ class Talk < ApplicationRecord # being a teachable (course/lecture/lesson), a talk has associated media has_many :media, -> { order(position: :asc) }, as: :teachable, - dependent: :destroy + dependent: :destroy, + inverse_of: :teachable # a talk has many tags has_many :talk_tag_joins, dependent: :destroy has_many :tags, through: :talk_tag_joins - after_save :remove_duplicate_dates + before_save :remove_duplicate_dates after_save :touch_lecture # the talks of a lecture form an ordered list @@ -31,7 +32,7 @@ def lesson end def to_label - I18n.t('talk', number: position, title: title) + I18n.t("talk", number: position, title: title) end def long_title @@ -52,7 +53,7 @@ def given_by?(user) def title_for_viewers Rails.cache.fetch("#{cache_key_with_version}/title_for_viewers") do - lecture.title_for_viewers + ', ' + to_label + "#{lecture.title_for_viewers}, #{to_label}" end end @@ -76,7 +77,7 @@ def media_scope end def compact_title - lecture.compact_title + '.V' + position.to_s + "#{lecture.compact_title}.V#{position}" end def number @@ -92,7 +93,7 @@ def next end def proper_media - media.where.not(sort: ['Question', 'Remark']) + media.where.not(sort: ["Question", "Remark"]) end def editors_with_inheritance @@ -111,6 +112,6 @@ def talk_path end def remove_duplicate_dates - update_columns(dates: dates.uniq) + dates.uniq! # TODO: replace dates array by a set to avoid this end end diff --git a/app/models/teachable_parser.rb b/app/models/teachable_parser.rb index 85793f256..2a3af0d1d 100644 --- a/app/models/teachable_parser.rb +++ b/app/models/teachable_parser.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # Teachable parser class # This is a service PORO model that is used in the media search class TeachableParser @@ -8,8 +6,8 @@ class TeachableParser # or 'Course-' followed by the id def initialize(params) @teachable_ids = params[:teachable_ids] || [] - @all_teachables = params[:all_teachables] == '1' - @inheritance = params[:teachable_inheritance] == '1' + @all_teachables = params[:all_teachables] == "1" + @inheritance = params[:teachable_inheritance] == "1" end # returns all courses, lectures and lessons that are associated @@ -30,8 +28,8 @@ def teachables_as_strings private def lecture_ids - @teachable_ids.select { |t| t.start_with?('Lecture') } - .map { |t| t.remove('Lecture-') }.map(&:to_i) + @teachable_ids.select { |t| t.start_with?("Lecture") } + .map { |t| t.remove("Lecture-") }.map(&:to_i) end def lectures @@ -39,8 +37,8 @@ def lectures end def course_ids - @teachable_ids.select { |t| t.start_with?('Course') } - .map { |t| t.remove('Course-') }.map(&:to_i) + @teachable_ids.select { |t| t.start_with?("Course") } + .map { |t| t.remove("Course-") }.map(&:to_i) end def courses diff --git a/app/models/term.rb b/app/models/term.rb index 36009cad4..28999bcea 100644 --- a/app/models/term.rb +++ b/app/models/term.rb @@ -4,8 +4,8 @@ class Term < ApplicationRecord has_many :lectures # season can only be SS/WS, and there can be only one of this type each year - validates :season, presence: true, - inclusion: { in: %w[SS WS] }, + validates :season, presence: true, # rubocop:todo Rails/UniqueValidationWithoutIndex + inclusion: { in: ["SS", "WS"] }, uniqueness: { scope: :year } # a year >=2000 needs to be present validates :year, presence: true, @@ -13,7 +13,7 @@ class Term < ApplicationRecord greater_than_or_equal_to: 2000 } # only one term can be active - validates_uniqueness_of :active, if: :active + validates :active, uniqueness: { if: :active } # some information about lectures, lessons and media are cached # to find out whether the cache is out of date, always touch'em after saving @@ -31,23 +31,23 @@ def active end def begin_date - season == 'SS' ? Date.new(year, 4, 1) : Date.new(year, 10, 1) + season == "SS" ? Date.new(year, 4, 1) : Date.new(year, 10, 1) end def end_date - season == 'SS' ? Date.new(year, 9, 30) : Date.new(year + 1, 3, 31) + season == "SS" ? Date.new(year, 9, 30) : Date.new(year + 1, 3, 31) end # label contains season and year(s) with all digits def to_label - return unless season.present? + return if season.blank? - season + ' ' + year_corrected + "#{season} #{year_corrected}" end # short label contains season and year(s) with two digits def to_label_short - season + ' ' + year_corrected_short + "#{season} #{year_corrected_short}" end def compact_title @@ -55,8 +55,8 @@ def compact_title end def previous - previous_year = season == 'WS' ? year : year - 1 - previous_season = season == 'WS' ? 'SS' : 'WS' + previous_year = season == "WS" ? year : year - 1 + previous_season = season == "WS" ? "SS" : "WS" Term.find_by(year: previous_year, season: previous_season) end @@ -102,48 +102,47 @@ def self.possible_deletion_dates end def self.possible_deletion_dates_localized - possible_deletion_dates.map { |d| - d.strftime(I18n.t('date.formats.concise')) - } + possible_deletion_dates.map do |d| + d.strftime(I18n.t("date.formats.concise")) + end end # array of all terms together with their ids for use in options_for_select - def self.select_terms(independent = false) - return ['bla', nil] if independent + def self.select_terms(independent: false) + return ["bla", nil] if independent Term.all.sort_by(&:begin_date).reverse.map { |t| [t.to_label, t.id] } end def self.previous_by_date(date) - season = date.month.in?((4..9)) ? 'SS' : 'WS' + season = date.month.in?((4..9)) ? "SS" : "WS" year = date.year - previous_year = season == 'WS' ? year : year - 1 - previous_season = season == 'WS' ? 'SS' : 'WS' + previous_year = season == "WS" ? year : year - 1 + previous_season = season == "WS" ? "SS" : "WS" Term.find_by(year: previous_year, season: previous_season) end private def year_corrected - return year.to_s unless season == 'WS' + return year.to_s unless season == "WS" - year.to_s + '/' + ((year % 100) + 1).to_s + "#{year}/#{(year % 100) + 1}" end def year_corrected_short - return (year % 100).to_s unless season == 'WS' + return (year % 100).to_s unless season == "WS" - (year % 100).to_s + '/' + ((year % 100) + 1).to_s + "#{year % 100}/#{(year % 100) + 1}" end def touch_lectures_and_lessons - lectures.update_all(updated_at: Time.now) - Lesson.where(lecture: lectures).update_all(updated_at: Time.now) + lectures.touch_all + Lesson.where(lecture: lectures).touch_all end def touch_media - Medium.where(teachable: lectures).update_all(updated_at: Time.now) - Medium.where(teachable: Lesson.where(lecture: lectures)) - .update_all(updated_at: Time.now) + Medium.where(teachable: lectures).touch_all + Medium.where(teachable: Lesson.where(lecture: lectures)).touch_all end end diff --git a/app/models/time_stamp.rb b/app/models/time_stamp.rb index 1924e36bb..8df7d5a06 100644 --- a/app/models/time_stamp.rb +++ b/app/models/time_stamp.rb @@ -8,9 +8,11 @@ class TimeStamp # extract from YAML def self.load(text) + return if text.blank? + YAML.safe_load(text, permitted_classes: [TimeStamp, ActiveModel::Errors], - aliases: true) if text.present? + aliases: true) end # store as YAML (for serialization) @@ -46,37 +48,45 @@ def initialize(params) # t.vtt_string # => "03:15:20.729" def vtt_string - format('%02d:%02d:%02d.%03d', @hours, @minutes, @seconds, @milliseconds) + # rubocop:disable Style/FormatStringToken + format("%02d:%02d:%02d.%03d", @hours, @minutes, @seconds, @milliseconds) + # rubocop:enable Style/FormatStringToken end # t.simple_vtt_string # => "3:15:20.729" def simple_vtt_string - format('%01d:%02d:%02d.%03d', @hours, @minutes, @seconds, @milliseconds) + # rubocop:disable Style/FormatStringToken + format("%01d:%02d:%02d.%03d", @hours, @minutes, @seconds, @milliseconds) + # rubocop:enable Style/FormatStringToken end # t.hms_string # => "3h15m20s" def hms_string - format('%01dh%02dm%02ds', @hours, @minutes, @seconds) + # rubocop:disable Style/FormatStringToken + format("%01dh%02dm%02ds", @hours, @minutes, @seconds) + # rubocop:enable Style/FormatStringToken end # t.hms_colon_string # => "3:15:20" def hms_colon_string - format('%01d:%02d:%02d', @hours, @minutes, @seconds) + # rubocop:disable Style/FormatStringToken + format("%01d:%02d:%02d", @hours, @minutes, @seconds) + # rubocop:enable Style/FormatStringToken end # t.floor_seconds # => 11720 def floor_seconds - @hours * 3600 + @minutes * 60 + @seconds + (@hours * 3600) + (@minutes * 60) + @seconds end # t.total_seconds # => 11720.729 def total_seconds - floor_seconds + @milliseconds / 1000.0 + floor_seconds + (@milliseconds / 1000.0) end private diff --git a/app/models/tutor_tutorial_join.rb b/app/models/tutor_tutorial_join.rb index 0e57281e6..416d289c8 100644 --- a/app/models/tutor_tutorial_join.rb +++ b/app/models/tutor_tutorial_join.rb @@ -1,4 +1,4 @@ class TutorTutorialJoin < ApplicationRecord belongs_to :tutorial - belongs_to :tutor, class_name: 'User', foreign_key: 'tutor_id' + belongs_to :tutor, class_name: "User", inverse_of: :tutor_tutorial_joins end diff --git a/app/models/tutorial.rb b/app/models/tutorial.rb index 60611c8a7..824f390cf 100644 --- a/app/models/tutorial.rb +++ b/app/models/tutorial.rb @@ -1,6 +1,6 @@ # Tutorial model class Tutorial < ApplicationRecord - require 'csv' + require "csv" belongs_to :lecture, touch: true @@ -11,10 +11,12 @@ class Tutorial < ApplicationRecord before_destroy :check_destructibility, prepend: true + # rubocop:todo Rails/UniqueValidationWithoutIndex validates :title, uniqueness: { scope: [:lecture_id] }, presence: true + # rubocop:enable Rails/UniqueValidationWithoutIndex def title_with_tutors - return "#{title}, #{I18n.t('basics.tba')}" unless tutors.any? + return "#{title}, #{I18n.t("basics.tba")}" unless tutors.any? "#{title}, #{tutor_names}" end @@ -22,7 +24,7 @@ def title_with_tutors def tutor_names return unless tutors.any? - tutors.map(&:tutorial_name).join(', ') + tutors.map(&:tutorial_name).join(", ") end def destructible? diff --git a/app/models/user.rb b/app/models/user.rb index d90b05bb8..7745f2260 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,7 +3,7 @@ class User < ApplicationRecord include ApplicationHelper # use devise for authentification, include the following modules - devise :database_authenticatable, :registerable, + devise :database_authenticatable, :registerable, :trackable, :recoverable, :rememberable, :validatable, :confirmable, :lockable # a user has many subscribed lectures @@ -17,39 +17,56 @@ class User < ApplicationRecord source: :lecture # a user has many courses as an editor - has_many :editable_user_joins, foreign_key: :user_id, dependent: :destroy + has_many :editable_user_joins, dependent: :destroy has_many :edited_courses, through: :editable_user_joins, - source: :editable, source_type: 'Course' + source: :editable, source_type: "Course" # a user has many lectures as an editor has_many :edited_lectures, through: :editable_user_joins, - source: :editable, source_type: 'Lecture' + source: :editable, source_type: "Lecture" # a user has many media as an editor has_many :edited_media, through: :editable_user_joins, - source: :editable, source_type: 'Medium' + source: :editable, source_type: "Medium" # a user has many lectures as a teacher - has_many :given_lectures, class_name: 'Lecture', foreign_key: 'teacher_id' + has_many :given_lectures, + class_name: "Lecture", + foreign_key: "teacher_id", + inverse_of: :teacher # a user has many tutorials as a tutor - has_many :tutor_tutorial_joins, foreign_key: 'tutor_id', dependent: :destroy + has_many :tutor_tutorial_joins, + foreign_key: "tutor_id", + dependent: :destroy, + inverse_of: :tutor has_many :given_tutorials, -> { order(:title) }, through: :tutor_tutorial_joins, source: :tutorial # a user has many given talks - has_many :speaker_talk_joins, foreign_key: 'speaker_id', dependent: :destroy + has_many :speaker_talk_joins, + foreign_key: "speaker_id", + dependent: :destroy, + inverse_of: :speaker has_many :talks, through: :speaker_talk_joins # a user has many notifications as recipient - has_many :notifications, foreign_key: 'recipient_id' + has_many :notifications, + foreign_key: "recipient_id", + inverse_of: :recipient # a user has many announcements as announcer - has_many :announcements, foreign_key: 'announcer_id', dependent: :destroy + has_many :announcements, + foreign_key: "announcer_id", + dependent: :destroy, + inverse_of: :announcer # a user has many clickers as editor - has_many :clickers, foreign_key: 'editor_id', dependent: :destroy + has_many :clickers, + foreign_key: "editor_id", + dependent: :destroy, + inverse_of: :editor # a user has many submissions (of assignments) has_many :user_submission_joins, dependent: :destroy @@ -75,10 +92,9 @@ class User < ApplicationRecord # set some default values before saving if they are not set before_save :set_defaults - before_destroy :destroy_single_submissions, prepend: true - # add timestamp for DSGVO consent after_create :set_consented_at + before_destroy :destroy_single_submissions, prepend: true # users can comment stuff acts_as_commontator @@ -104,7 +120,7 @@ class User < ApplicationRecord # returns the array of all teachers def self.teachers - User.where(id: Lecture.pluck(:teacher_id).uniq) + User.where(id: Lecture.distinct.select(:teacher_id)) end def self.select_teachers @@ -113,13 +129,13 @@ def self.select_teachers # returns the array of all editors def self.editors - User.where(id: EditableUserJoin.pluck(:user_id).uniq) + User.where(id: EditableUserJoin.distinct.select(:user_id)) end # returns the array of all editors minus those that are only editors of talks def self.proper_editors - talk_media_ids = Medium.where(teachable_type: 'Talk').pluck(:id) - talk_media_joins = EditableUserJoin.where(editable_type: 'Medium', + talk_media_ids = Medium.where(teachable_type: "Talk").pluck(:id) + talk_media_joins = EditableUserJoin.where(editable_type: "Medium", editable_id: talk_media_ids) User.where(id: EditableUserJoin.where.not(id: talk_media_joins.pluck(:id)) .pluck(:user_id).uniq) @@ -136,7 +152,7 @@ def self.only_editors_selection # given array of ids # search params is a hash having keys :all_editors, :editor_ids def self.search_editors(search_params) - return User.editors unless search_params[:all_editors] == '0' + return User.editors unless search_params[:all_editors] == "0" editor_ids = search_params[:editor_ids] || [] User.where(id: editor_ids) @@ -165,18 +181,18 @@ def self.preferred_name_or_email_like(search_string) return User.none unless search_string return User.none unless search_string.length >= 2 - where(name_in_tutorials: [nil, '']).name_or_email_like(search_string) + where(name_in_tutorials: [nil, ""]).name_or_email_like(search_string) .or(where.not(name_in_tutorials: [nil, - '']) + ""]) .name_in_tutorials_or_email_like(search_string)) end def self.values_for_select pluck(:id, :name, :name_in_tutorials, :email) - .map { |u| + .map do |u| { value: u.first, text: "#{u.third.presence || u.second} (#{u.fourth})" } - } + end end def courses @@ -192,10 +208,8 @@ def related_courses(overrule_subscription_type: false) return if subscription_type.nil? selection_type = overrule_subscription_type || subscription_type - if selection_type == 1 - return Course.where(id: preceding_course_ids).includes(:lectures) - end - return Course.all.includes(:lectures) if selection_type == 2 + return Course.where(id: preceding_course_ids).includes(:lectures) if selection_type == 1 + return Course.includes(:lectures) if selection_type == 2 courses end @@ -220,10 +234,10 @@ def related_lectures # returns ARel of all those tags from the given tags that belong to # the user's related lectures def filter_tags(tags) - Tag.where(id: tags.select { |t| + Tag.where(id: tags.select do |t| t.in_lectures?(related_lectures) || t.in_courses?(related_courses) - } + end .map(&:id)) end @@ -278,7 +292,7 @@ def active_notifications(lecture) end def active_media_notifications(lecture) - notifications.where(notifiable_type: 'Medium') + notifications.where(notifiable_type: "Medium") .where(notifiable_id: lecture.media_with_inheritance .pluck(:id)) end @@ -286,7 +300,7 @@ def active_media_notifications(lecture) # returns the array of those notifications that are related to MaMpf news # (i.e. announcements without a lecture) def active_news - notifications.where(notifiable_type: 'Announcement') + notifications.where(notifiable_type: "Announcement") .select { |n| n.notifiable.lecture.nil? } end @@ -323,24 +337,23 @@ def active_teachable_editor? return false unless can_edit_teachables? return true if admin || course_editor? || teacher? - edited_lectures.select { |l| l.term.nil? || !l.stale? } - .any? + edited_lectures.any? { |l| l.term.nil? || !l.stale? } end # a user is an editor iff he/she is a teachable editor or an # editor of media that are not associated to talks def editor? teachable_editor? || - edited_media.where.not(teachable_type: 'Talk').any? + edited_media.where.not(teachable_type: "Talk").any? end # the next methods return information about the user extracted from # email and name def info_uncached - return email unless name.present? + return email if name.blank? - (name_in_tutorials.presence || name) + ' (' + email + ')' + "#{name_in_tutorials.presence || name} (#{email})" end def info @@ -350,9 +363,9 @@ def info end def tutorial_info_uncached - return email unless tutorial_name.present? + return email if tutorial_name.blank? - tutorial_name + ' (' + email + ')' + "#{tutorial_name} (#{email})" end def tutorial_info @@ -362,7 +375,7 @@ def tutorial_info end def name_or_email - return name unless name.blank? + return name if name.present? email end @@ -372,7 +385,7 @@ def tutorial_name end def short_info - return email unless name.present? + return email if name.blank? name end @@ -497,7 +510,7 @@ def filter_visible_media(media) Course.where(id: Course.pluck(:id) - courses.pluck(:id)) nonsubscribed_lectures = Lecture.where(id: Lecture.pluck(:id) - lectures.pluck(:id), - released: ['all']) + released: ["all"]) lessons = Lesson.where(lecture: lectures) nonsubscribed_lessons = Lesson.where(lecture: nonsubscribed_lectures) edited_lessons = Lesson.where(lecture: teaching_related_lectures) @@ -506,21 +519,21 @@ def filter_visible_media(media) edited_talks = Talk.where(lecture: teaching_related_lectures) return media if admin - media.where(teachable: courses, released: ['all', 'subscribers', 'users']) + media.where(teachable: courses, released: ["all", "subscribers", "users"]) .or(media.where(teachable: nonsubscribed_courses, - released: ['all', 'users'])) + released: ["all", "users"])) .or(media.where(teachable: lectures, - released: ['all', 'subscribers', 'users'])) + released: ["all", "subscribers", "users"])) .or(media.where(teachable: nonsubscribed_lectures, - released: ['all', 'users'])) + released: ["all", "users"])) .or(media.where(teachable: lessons, - released: ['all', 'subscribers', 'users'])) + released: ["all", "subscribers", "users"])) .or(media.where(teachable: nonsubscribed_lessons, - released: ['all', 'users'])) + released: ["all", "users"])) .or(media.where(teachable: talks, - released: ['all', 'subscribers', 'users'])) + released: ["all", "subscribers", "users"])) .or(media.where(teachable: nonsubscribed_talks, - released: ['all', 'users'])) + released: ["all", "users"])) .or(media.where(teachable: edited_courses)) .or(media.where(teachable: teaching_related_lectures)) .or(media.where(teachable: edited_lessons)) @@ -529,23 +542,22 @@ def filter_visible_media(media) def subscribed_commentable_media_with_comments lessons = Lesson.where(lecture: lectures) - filter_media(Medium.where.not(sort: ['RandomQuiz', 'Question', 'Erdbeere', - 'Remark']) + filter_media(Medium.where.not(sort: ["RandomQuiz", "Question", "Erdbeere", + "Remark"]) .where(teachable: courses + lectures + lessons)) .includes(commontator_thread: :comments) .select { |m| m.commontator_thread.comments.any? } end def media_latest_comments - subscribed_commentable_media_with_comments - .map { |m| + media = subscribed_commentable_media_with_comments + .map do |m| { medium: m, thread: m.commontator_thread, latest_comment: m.commontator_thread - .comments.sort_by(&:created_at) - .last } - } - .sort_by { |x| x[:latest_comment].created_at }.reverse + .comments.max_by(&:created_at) } + end + media.sort_by { |x| x[:latest_comment].created_at }.reverse end # lecture that are in the active term @@ -572,7 +584,7 @@ def subscribe_lecture!(lecture) lectures << lecture # make sure subscribed_users is updated in media - Sunspot.index! lecture.media + Sunspot.index!(lecture.media) true end @@ -585,7 +597,7 @@ def unsubscribe_lecture!(lecture) favorite_lectures.delete(lecture) # make sure subscribed_users is updated in media - Sunspot.index! lecture.media + Sunspot.index!(lecture.media) true end @@ -640,16 +652,12 @@ def tutorials(lecture) given_tutorials.where(lecture: lecture) end - def has_tutorials?(lecture) - !given_tutorials.where(lecture: lecture).empty? - end - def proper_submissions_count submissions.proper.size end def proper_single_submissions_count - submissions.proper.select { |s| s.users.size == 1 }.size + submissions.proper.count { |s| s.users.size == 1 } end def proper_team_submissions_count @@ -692,26 +700,26 @@ def normalized_image_url_with_host def image_filename return unless image - image.metadata['filename'] + image.metadata["filename"] end def image_size return unless image - image.metadata['size'] + image.metadata["size"] end def image_resolution return unless image - "#{image.metadata['width']}x#{image.metadata['height']}" + "#{image.metadata["width"]}x#{image.metadata["height"]}" end def can_edit?(something) unless something.is_a?(Lecture) || something.is_a?(Course) || something.is_a?(Medium) || something.is_a?(Lesson) || something.is_a?(Talk) - raise 'can_edit? was called with incompatible class' + raise("can_edit? was called with incompatible class") end return true if admin @@ -723,9 +731,9 @@ def speaker? end def layout - return 'administration' if admin_or_editor? + return "administration" if admin_or_editor? - 'application_no_sidebar' + "application_no_sidebar" end def course_editor? @@ -746,9 +754,24 @@ def can_update_personell?(lecture) return false unless can_edit?(lecture) return true if can_edit?(lecture.course) || lecture.teacher == self return true if lecture.course.term_independent - return true if !lecture.stale? + return true unless lecture.stale? + + false + end + + # see https://github.com/heartcombo/devise/issues/4849#issuecomment-534733131 + # We use the Devise::Trackable module to track sign-in count and current/last + # sign-in timestamp. However, we don't want to track IP address, but Trackable + # tries to, so we have to manually override the accessor methods so they do + # nothing. + + def current_sign_in_ip + end + + def last_sign_in_ip=(_ip) + end - return false + def current_sign_in_ip=(_ip) end private @@ -756,19 +779,19 @@ def can_update_personell?(lecture) def set_defaults self.subscription_type ||= 1 self.admin ||= false - self.name ||= email.split('@').first + self.name ||= email.split("@").first self.locale ||= I18n.default_locale.to_s end # sets time for DSGVO consent to current time def set_consented_at - update(consented_at: Time.now) + update(consented_at: Time.zone.now) end # returns array of ids of all courses that preced the subscribed courses def preceding_course_ids courses.all.map { |l| l.preceding_courses.pluck(:id) }.flatten + - courses.all.pluck(:id) + courses.pluck(:id) end def destroy_single_submissions @@ -777,16 +800,16 @@ def destroy_single_submissions end def archive_email - splitting = DefaultSetting::PROJECT_EMAIL.split('@') + splitting = DefaultSetting::PROJECT_EMAIL.split("@") "#{splitting.first}-archive-#{id}@#{splitting.second}" end def transfer_contributions_to(user) - return false unless user && user.valid? && user != self + return false unless user&.valid? && user != self - given_lectures.update_all(teacher_id: user.id) - EditableUserJoin.where(user: self, editable_type: 'Medium') - .update_all(user_id: user.id) + given_lectures.update(teacher_id: user.id) + EditableUserJoin.where(user: self, editable_type: "Medium") + .update(user_id: user.id) end def archive_user(archive_name) @@ -794,8 +817,8 @@ def archive_user(archive_name) email: archive_email, password: SecureRandom.base58(12), consents: true, - consented_at: Time.now, - confirmed_at: Time.now, + consented_at: Time.zone.now, + confirmed_at: Time.zone.now, archived: true) end end diff --git a/app/models/user_cleaner.rb b/app/models/user_cleaner.rb index 9c40eecba..877760420 100644 --- a/app/models/user_cleaner.rb +++ b/app/models/user_cleaner.rb @@ -3,9 +3,9 @@ class UserCleaner attr_accessor :imap, :email_dict, :hash_dict def login - @imap = Net::IMAP.new(ENV['IMAPSERVER'], port: 993, ssl: true) - @imap.authenticate('LOGIN', ENV['PROJECT_EMAIL_USERNAME'], - ENV['PROJECT_EMAIL_PASSWORD']) + @imap = Net::IMAP.new(ENV.fetch("IMAPSERVER", nil), port: 993, ssl: true) + @imap.authenticate("LOGIN", ENV.fetch("PROJECT_EMAIL_USERNAME", nil), + ENV.fetch("PROJECT_EMAIL_PASSWORD", nil)) end def logout @@ -15,40 +15,44 @@ def logout def search_emails_and_hashes @email_dict = {} @hash_dict = {} - @imap.examine(ENV['PROJECT_EMAIL_MAILBOX']) + @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX", nil)) # Mails containing multiple email addresses (Subject: "Undelivered Mail Returned to Sender") - @imap.search(['SUBJECT', - 'Undelivered Mail Returned to Sender']).each do |message_id| + @imap.search(["SUBJECT", + "Undelivered Mail Returned to Sender"]).each do |message_id| body = @imap.fetch(message_id, "BODY[TEXT]")[0].attr["BODY[TEXT]"].squeeze(" ") - if match = body.scan(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4})[\s\S]*?([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4})[\s\S]*?User has moved to ERROR: Account expired/) - match = match.flatten.uniq - match.each do |email| - add_mail(email, message_id) + # rubocop:disable Layout/LineLength + next unless (match = body.scan(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4})[\s\S]*?([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4})[\s\S]*?User has moved to ERROR: Account expired/)) + # rubocop:enable Layout/LineLength - try_get_hash(body, email) - end + match = match.flatten.uniq + match.each do |email| + add_mail(email, message_id) + + try_get_hash(body, email) end end # Mails containing single email addresses (Subject: "Delivery Status Notification (Failure)") # define array containing all used regex patterns patterns = [ '([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4})>[\s\S]*?Unknown recipient', + # rubocop:disable Layout/LineLength '([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4})>[\s\S]*?User unknown in virtual mailbox table' + # rubocop:enable Layout/LineLength ] - @imap.search(['SUBJECT', - 'Delivery Status Notification (Failure)']).each do |message_id| + @imap.search(["SUBJECT", + "Delivery Status Notification (Failure)"]).each do |message_id| body = @imap.fetch(message_id, "BODY[TEXT]")[0].attr["BODY[TEXT]"].squeeze(" ") patterns.each do |pattern| - if match = body.scan(/#{pattern}/) - match = match.flatten.uniq - match.each do |email| - add_mail(email, message_id) + next unless (match = body.scan(/#{pattern}/)) - try_get_hash(body, email) - end + match = match.flatten.uniq + match.each do |email| + add_mail(email, message_id) + + try_get_hash(body, email) end end end @@ -68,8 +72,8 @@ def try_get_hash(body, email) begin hash = body.match(/Hash:([a-zA-Z0-9]*)/).captures @hash_dict[email] = hash - rescue - return + rescue StandardError + nil end end @@ -87,7 +91,7 @@ def send_hashes def delete_ghosts @hash_dict.each do |mail, hash| u = User.find_by(email: mail, ghost_hash: hash) - move_mail(@email_dict[mail]) if u.present? and @email_dict.present? + move_mail(@email_dict[mail]) if u.present? && @email_dict.present? u.destroy! if u&.generic? end end @@ -96,15 +100,13 @@ def move_mail(message_ids, attempt = 0) return if message_ids.blank? message_ids = Array(message_ids) - if attempt > 3 - return - end + return if attempt > 3 begin - @imap.examine(ENV['PROJECT_EMAIL_MAILBOX']) + @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX", nil)) @imap.move(message_ids, "Other Users/mampf/handled_bounces") rescue Net::IMAP::BadResponseError - move_mail(message_ids, attempt = attempt + 1) + move_mail(message_ids, attempt + 1) end end diff --git a/app/models/user_submission_join.rb b/app/models/user_submission_join.rb index 9218519c6..92e0f6c61 100644 --- a/app/models/user_submission_join.rb +++ b/app/models/user_submission_join.rb @@ -5,9 +5,7 @@ class UserSubmissionJoin < ApplicationRecord validate :only_one_per_assignment, on: :create validate :max_team_size, on: :create - def assignment - submission.assignment - end + delegate :assignment, to: :submission private diff --git a/app/models/watchlist.rb b/app/models/watchlist.rb index 1602b95be..d64b358b1 100644 --- a/app/models/watchlist.rb +++ b/app/models/watchlist.rb @@ -3,9 +3,11 @@ class Watchlist < ApplicationRecord has_many :watchlist_entries, dependent: :destroy has_many :media, through: :watchlist_entries + # rubocop:todo Rails/UniqueValidationWithoutIndex validates :name, presence: true, uniqueness: { scope: :user_id } + # rubocop:enable Rails/UniqueValidationWithoutIndex - def owned_by?(otherUser) - user == otherUser + def owned_by?(other_user) + user == other_user end end diff --git a/app/models/watchlist_entry.rb b/app/models/watchlist_entry.rb index 104c7f755..47bebdf87 100644 --- a/app/models/watchlist_entry.rb +++ b/app/models/watchlist_entry.rb @@ -4,7 +4,7 @@ class WatchlistEntry < ApplicationRecord belongs_to :medium acts_as_list scope: :watchlist, top_of_list: 0, column: :medium_position - validates :medium, presence: true - validates :watchlist, presence: true + # rubocop:todo Rails/UniqueValidationWithoutIndex validates :medium_id, uniqueness: { scope: :watchlist_id } + # rubocop:enable Rails/UniqueValidationWithoutIndex end diff --git a/app/models/xkcd.rb b/app/models/xkcd.rb index 55752c45b..6ede1962b 100644 --- a/app/models/xkcd.rb +++ b/app/models/xkcd.rb @@ -1,7 +1,7 @@ # Xkcd model for getting Xkcd images class Xkcd def self.random - max = JSON.parse(URI.open('https://xkcd.com/info.0.json').read)['num'] + max = JSON.parse(URI.open("https://xkcd.com/info.0.json").read)["num"] comic_num = 1 + rand(max - 1) comic_num = 1 if comic_num == 404 # Avoid 404th comic ;) JSON.parse(URI.open("https://xkcd.com/#{comic_num}/info.0.json").read) diff --git a/app/uploaders/correction_uploader.rb b/app/uploaders/correction_uploader.rb index 6653ad459..332ad1216 100644 --- a/app/uploaders/correction_uploader.rb +++ b/app/uploaders/correction_uploader.rb @@ -1,4 +1,4 @@ -require 'image_processing/mini_magick' +require "image_processing/mini_magick" # UserPdfUploader Class class CorrectionUploader < Shrine @@ -11,6 +11,6 @@ class CorrectionUploader < Shrine Attacher.validate do # Reject empty file uploads # at least 1 byte - validate_min_size 1, message: I18n.t('submission.upload_failure_empty_file') + validate_min_size 1, message: I18n.t("submission.upload_failure_empty_file") end end diff --git a/app/uploaders/geogebra_uploader.rb b/app/uploaders/geogebra_uploader.rb index 32fe00ff5..27bf968f4 100644 --- a/app/uploaders/geogebra_uploader.rb +++ b/app/uploaders/geogebra_uploader.rb @@ -1,4 +1,4 @@ -require 'zip' +require "zip" # GeogebraUploader class # used for storing geogebra files @@ -9,17 +9,17 @@ class GeogebraUploader < Shrine plugin :derivatives Attacher.validate do - validate_mime_type_inclusion %w[application/zip], - message: 'falscher MIME-Typ' + validate_mime_type_inclusion ["application/zip"], + message: "falscher MIME-Typ" end # extract a screenshot from the ggb file and store it beside the ggb file Attacher.derivatives_processor do |original| - unzipped = '' + unzipped = "" Zip::File.open(original) do |zip_file| - destination = Dir.mktmpdir('geogebra') - zipped = zip_file.find { |f| f.name == 'geogebra_thumbnail.png' } - unzipped = File.join(destination, 'geogebra_thumbnail.png') + destination = Dir.mktmpdir("geogebra") + zipped = zip_file.find { |f| f.name == "geogebra_thumbnail.png" } + unzipped = File.join(destination, "geogebra_thumbnail.png") zip_file.extract(zipped, unzipped) end { screenshot: File.open(unzipped) } diff --git a/app/uploaders/pdf_uploader.rb b/app/uploaders/pdf_uploader.rb index bacdb9bcc..a79b7d9ff 100644 --- a/app/uploaders/pdf_uploader.rb +++ b/app/uploaders/pdf_uploader.rb @@ -1,4 +1,4 @@ -require 'image_processing/mini_magick' +require "image_processing/mini_magick" # PdfUploader Class class PdfUploader < Shrine @@ -20,7 +20,7 @@ class PdfUploader < Shrine temp_file = Tempfile.new temp_folder = Dir.mktmpdir structure_path = "#{temp_folder}/structure.mampf" - cmd = "pdftk #{file.path} dump_data_utf8 output #{temp_file.path} && "\ + cmd = "pdftk #{file.path} dump_data_utf8 output #{temp_file.path} && " \ "pdftk #{file.path} unpack_files output #{temp_folder}" exit_status = system(cmd) if exit_status @@ -30,11 +30,11 @@ class PdfUploader < Shrine # extract lines that correspond to MaMpf-Label entries from LaTEX # package mampf.sty structure = if File.file?(structure_path) - open(structure_path, "r") do |io| + File.open(structure_path, "r") do |io| io.read.encode("UTF-8", invalid: :replace) end end - structure ||= '' + structure ||= "" bookmarks = structure.scan(/MaMpf-Label\|(.*?)\n/).flatten result = [] bookmarks.each_with_index do |b, i| @@ -42,40 +42,40 @@ class PdfUploader < Shrine # line may look like this: # defn:erster-Tag|Definition|1.1|Erster Tag|1 data = /(.*?)\|(.*?)\|(.*?)\|(.*?)\|(.*)\|(.*)\|(.*)\|(.*)/.match(b) - details = { 'destination' => data[1], 'sort' => data[2], - 'label' => data[3], 'description' => data[4], - 'chapter' => data[5], 'section' => data[6], - 'subsection' => data[7], 'page' => data[8], - 'counter' => i } - details['sort'] = 'Markierung' if details['sort'].blank? + details = { "destination" => data[1], "sort" => data[2], + "label" => data[3], "description" => data[4], + "chapter" => data[5], "section" => data[6], + "subsection" => data[7], "page" => data[8], + "counter" => i } + details["sort"] = "Markierung" if details["sort"].blank? result.push(details) end linked_media = structure.scan(/MaMpf-Link\|(.*?)\n/) .flatten.map(&:to_i) - [0] mampf_sty_version = structure.scan(/MaMpf-Version\|(.*?)\n/).flatten .first - { 'pages' => pages, - 'destinations' => result.map { |b| b['destination'] }, - 'bookmarks' => result, - 'linked_media' => linked_media, - 'version' => mampf_sty_version } + { "pages" => pages, + "destinations" => result.pluck("destination"), + "bookmarks" => result, + "linked_media" => linked_media, + "version" => mampf_sty_version } else - { 'pages' => nil, 'destinations' => nil, 'bookmarks' => nil, - 'version' => nil } + { "pages" => nil, "destinations" => nil, "bookmarks" => nil, + "version" => nil } end end end end Attacher.validate do - validate_mime_type_inclusion %w[application/pdf], - message: 'falscher MIME-Typ' + validate_mime_type_inclusion ["application/pdf"], + message: "falscher MIME-Typ" end # extract a screenshot from pdf and store it beside the pdf Attacher.derivatives_processor do |original| screenshot = ImageProcessing::MiniMagick.source(original).loader(page: 0) - .convert('png') + .convert("png") .resize_to_limit!(400, 565) { screenshot: screenshot } end diff --git a/app/uploaders/profileimage_uploader.rb b/app/uploaders/profileimage_uploader.rb index 18aac9e70..aa5f86a64 100644 --- a/app/uploaders/profileimage_uploader.rb +++ b/app/uploaders/profileimage_uploader.rb @@ -1,4 +1,4 @@ -require 'image_processing/mini_magick' +require "image_processing/mini_magick" # ProfileimageUploader class # used for storing profile images class ProfileimageUploader < Shrine @@ -11,8 +11,8 @@ class ProfileimageUploader < Shrine plugin :derivatives Attacher.validate do - validate_mime_type_inclusion %w[image/jpeg image/png image/gif], - message: 'falscher MIME-Typ' + validate_mime_type_inclusion ["image/jpeg", "image/png", "image/gif"], + message: "falscher MIME-Typ" end # store a resized version of the screenshot diff --git a/app/uploaders/screenshot_uploader.rb b/app/uploaders/screenshot_uploader.rb index 9db2a7a98..6ef5cbef7 100644 --- a/app/uploaders/screenshot_uploader.rb +++ b/app/uploaders/screenshot_uploader.rb @@ -1,4 +1,4 @@ -require 'image_processing/mini_magick' +require "image_processing/mini_magick" # ScreenshotUploader class # used for storing video thumbnails class ScreenshotUploader < Shrine @@ -11,8 +11,8 @@ class ScreenshotUploader < Shrine plugin :derivatives Attacher.validate do - validate_mime_type_inclusion %w[image/jpeg image/png image/gif], - message: 'falscher MIME-Typ' + validate_mime_type_inclusion ["image/jpeg", "image/png", "image/gif"], + message: "falscher MIME-Typ" end # store a resized version of the screenshot diff --git a/app/uploaders/submission_uploader.rb b/app/uploaders/submission_uploader.rb index ad029ab3d..afeb16e87 100644 --- a/app/uploaders/submission_uploader.rb +++ b/app/uploaders/submission_uploader.rb @@ -1,4 +1,4 @@ -require 'image_processing/mini_magick' +require "image_processing/mini_magick" # SubmissionUploader Class class SubmissionUploader < Shrine @@ -12,6 +12,6 @@ class SubmissionUploader < Shrine Attacher.validate do # Reject empty file uploads # at least 1 byte - validate_min_size 1, message: I18n.t('submission.upload_failure_empty_file') + validate_min_size 1, message: I18n.t("submission.upload_failure_empty_file") end end diff --git a/app/uploaders/video_uploader.rb b/app/uploaders/video_uploader.rb index 4e42b3134..bcfe7aff2 100644 --- a/app/uploaders/video_uploader.rb +++ b/app/uploaders/video_uploader.rb @@ -1,4 +1,4 @@ -require 'streamio-ffmpeg' +require "streamio-ffmpeg" # VideoUploader class class VideoUploader < Shrine @@ -12,18 +12,17 @@ class VideoUploader < Shrine # add metadata to uploaded video: duration, bitrate, resolution, framerate add_metadata do |io, **options| - pp options[:action] if options[:action] != :upload movie = Shrine.with_file(io) { |file| FFMPEG::Movie.new(file.path) } - { 'duration' => movie.duration, - 'bitrate' => movie.bitrate, - 'resolution' => movie.resolution, - 'frame_rate' => movie.frame_rate } + { "duration" => movie.duration, + "bitrate" => movie.bitrate, + "resolution" => movie.resolution, + "frame_rate" => movie.frame_rate } end end Attacher.validate do - validate_mime_type_inclusion %w[video/mp4], message: 'wrong type' + validate_mime_type_inclusion ["video/mp4"], message: "wrong type" end end diff --git a/app/uploaders/zip_uploader.rb b/app/uploaders/zip_uploader.rb index 353280167..9e361ddc2 100644 --- a/app/uploaders/zip_uploader.rb +++ b/app/uploaders/zip_uploader.rb @@ -7,11 +7,11 @@ class ZipUploader < Shrine plugin :default_storage, cache: :submission_cache, store: :submission_store Attacher.validate do - validate_mime_type_inclusion %w[application/zip], + validate_mime_type_inclusion ["application/zip"], message: - I18n.t('package.no_zip') + I18n.t("package.no_zip") # maximum size of 1 GB validate_max_size 1024 * 1024 * 1024, - message: I18n.t('package.too_big') + message: I18n.t("package.too_big") end end diff --git a/app/validators/http_url_validator.rb b/app/validators/http_url_validator.rb index 7dcf6ac83..9c5ed15eb 100644 --- a/app/validators/http_url_validator.rb +++ b/app/validators/http_url_validator.rb @@ -7,8 +7,8 @@ def self.compliant?(value) end def validate_each(record, attribute, value) - unless value.present? && self.class.compliant?(value) - record.errors.add(attribute, I18n.t('activerecord.errors.no_valid_url')) - end + return if value.present? && self.class.compliant?(value) + + record.errors.add(attribute, I18n.t("activerecord.errors.no_valid_url")) end end diff --git a/app/views/assignments/new.js.erb b/app/views/assignments/new.js.erb index c7872b491..3f1853678 100644 --- a/app/views/assignments/new.js.erb +++ b/app/views/assignments/new.js.erb @@ -1,18 +1,18 @@ -$('#newAssignmentButton').hide(); +$("#newAssignmentButton").hide(); -$('#assignmentListHeader').show() - .after('<%= j render partial: "assignments/form", locals: { assignment: @assignment } %>'); +$("#assignmentListHeader").show() + .after("<%= j render partial: "assignments/form", locals: { assignment: @assignment } %>"); -new TomSelect('#assignment_medium_id_', { +new TomSelect("#assignment_medium_id_", { sortField: { - field: 'text', - direction: 'asc' + field: "text", + direction: "asc", }, render: { - no_results: function(data, escape) { + no_results: function (_data, _escape) { return '
<%= t("basics.no_results") %>
'; - } - } + }, + }, }); -$('#assignment_medium_id_').val(null).trigger('change'); +$("#assignment_medium_id_").val(null).trigger("change"); diff --git a/app/views/clickers/open.coffee b/app/views/clickers/open.coffee index eed12a7ae..5de1b12f2 100644 --- a/app/views/clickers/open.coffee +++ b/app/views/clickers/open.coffee @@ -1,5 +1,5 @@ getClickerVotes = -> - $.ajax Routes.get_votes_count_path(<%= @clicker.id %>), + $.ajax Routes.votes_count_path(<%= @clicker.id %>), type: 'GET' dataType: 'json' success: (result) -> diff --git a/app/views/commontator/comments/cancel.js.erb b/app/views/commontator/comments/cancel.js.erb index d9f34dcc6..71688f1d6 100644 --- a/app/views/commontator/comments/cancel.js.erb +++ b/app/views/commontator/comments/cancel.js.erb @@ -1,15 +1,15 @@ <% if @comment.nil? || @comment.new_record? %> - <% +<% id = @comment.nil? || @comment.parent.nil? ? "commontator-thread-#{@commontator_thread.id}-new-comment" : "commontator-comment-#{@comment.parent.id}-reply" %> - $("#<%= id %>").hide(); +$("#<%= id %>").hide(); - $("#<%= id %>-link").fadeIn(); +$("#<%= id %>-link").fadeIn(); <% else %> - $("#commontator-comment-<%= @comment.id %>-body").html("<%= escape_javascript( +$("#commontator-comment-<%= @comment.id %>-body").html("<%= escape_javascript( render partial: 'body', locals: { comment: @comment } ) %>"); <% end %> diff --git a/app/views/commontator/comments/create.js.erb b/app/views/commontator/comments/create.js.erb index 4754a6fef..3d9cbb2e1 100644 --- a/app/views/commontator/comments/create.js.erb +++ b/app/views/commontator/comments/create.js.erb @@ -18,11 +18,11 @@ %> <% if @commontator_new_comment.nil? %> - $("#<%= id %>").hide(); +$("#<%= id %>").hide(); - $("#<%= id %>-link").fadeIn(); +$("#<%= id %>-link").fadeIn(); <% else %> - $("#<%= id %>").html("<%= escape_javascript( +$("#<%= id %>").html("<%= escape_javascript( render partial: 'form', locals: { comment: @commontator_new_comment, thread: @commontator_thread } @@ -30,12 +30,12 @@ <% end %> <% if @update_icon %> -$('#commentsIcon').addClass('new-comment'); +$("#commentsIcon").addClass("new-comment"); <% end %> var commontatorComment = $("#commontator-comment-<%= @comment.id %>").hide().fadeIn(); -$('html, body').animate( - { scrollTop: commontatorComment.offset().top - window.innerHeight/2 }, 'fast' +$("html, body").animate( + { scrollTop: commontatorComment.offset().top - window.innerHeight / 2 }, "fast", ); <%= javascript_proc %> diff --git a/app/views/commontator/comments/edit.js.erb b/app/views/commontator/comments/edit.js.erb index 4bb3f28e0..0714bd3e3 100644 --- a/app/views/commontator/comments/edit.js.erb +++ b/app/views/commontator/comments/edit.js.erb @@ -2,6 +2,6 @@ $("#commontator-comment-<%= @comment.id %>-body").html("<%= escape_javascript( render partial: 'form', locals: { comment: @comment } ) %>"); -$('#commontator-comment-<%= @comment.id %>-edit-body').focus(); +$("#commontator-comment-<%= @comment.id %>-edit-body").focus(); <%= javascript_proc %> diff --git a/app/views/commontator/comments/new.js.erb b/app/views/commontator/comments/new.js.erb index 0ec489036..38ad2afbe 100644 --- a/app/views/commontator/comments/new.js.erb +++ b/app/views/commontator/comments/new.js.erb @@ -6,11 +6,11 @@ var commontatorForm = $("#<%= id %>").html("<%= escape_javascript( render partial: 'form', locals: { comment: @comment, thread: @commontator_thread } ) %>").hide().fadeIn(); -$('html, body').animate({ scrollTop: commontatorForm.offset().top - window.innerHeight/2 }, 'fast'); +$("html, body").animate({ scrollTop: commontatorForm.offset().top - window.innerHeight / 2 }, "fast"); -initBootstrapPopovers() +initBootstrapPopovers(); $("#<%= id %>-link").hide(); -$('#<%= id %>-body').focus(); +$("#<%= id %>-body").focus(); <%= javascript_proc %> diff --git a/app/views/commontator/comments/show.js.erb b/app/views/commontator/comments/show.js.erb index 0274f80c9..ca0dc8332 100644 --- a/app/views/commontator/comments/show.js.erb +++ b/app/views/commontator/comments/show.js.erb @@ -1,8 +1,8 @@ var commontatorOldCommentIds = $("#commontator-comment-<%= @comment.id -%>-children").children().map(function() { - return '#' + $(this).attr('id'); -}).toArray().join(','); +%>-children").children().map(function () { + return "#" + $(this).attr("id"); +}).toArray().join(","); <%= render partial: 'show', locals: { @@ -17,8 +17,8 @@ var commontatorOldCommentIds = $("#commontator-comment-<%= var commontatorNewComments = $("#commontator-comment-<%= @comment.id %>-children").children().not(commontatorOldCommentIds).hide().fadeIn(); -$('html, body').animate( - { scrollTop: commontatorNewComments.offset().top - window.innerHeight/2 }, 'fast' +$("html, body").animate( + { scrollTop: commontatorNewComments.offset().top - window.innerHeight / 2 }, "fast", ); <%= javascript_proc %> diff --git a/app/views/commontator/threads/_hide_show_links.js.erb b/app/views/commontator/threads/_hide_show_links.js.erb index 2d5dd862d..8b8f405d4 100644 --- a/app/views/commontator/threads/_hide_show_links.js.erb +++ b/app/views/commontator/threads/_hide_show_links.js.erb @@ -3,19 +3,19 @@ thread %> -$("#commontator-thread-<%= thread.id %>-hide-link").click(function() { +$("#commontator-thread-<%= thread.id %>-hide-link").click(function () { $("#commontator-thread-<%= thread.id %>-content").hide(); var commontatorLink = $("#commontator-thread-<%= thread.id %>-show").fadeIn(); - $('html, body').animate( - { scrollTop: commontatorLink.offset().top - window.innerHeight/2 }, 'fast' + $("html, body").animate( + { scrollTop: commontatorLink.offset().top - window.innerHeight / 2 }, "fast", ); }); -$("#commontator-thread-<%= thread.id %>-show-link").click(function() { +$("#commontator-thread-<%= thread.id %>-show-link").click(function () { var commontatorThread = $("#commontator-thread-<%= thread.id %>-content").fadeIn(); - $('html, body').animate( - { scrollTop: commontatorThread.offset().top - window.innerHeight/2 }, 'fast' + $("html, body").animate( + { scrollTop: commontatorThread.offset().top - window.innerHeight / 2 }, "fast", ); $("#commontator-thread-<%= thread.id %>-show").hide(); diff --git a/app/views/commontator/threads/_show.js.erb b/app/views/commontator/threads/_show.js.erb index 8c94ea3d7..777c19d0c 100644 --- a/app/views/commontator/threads/_show.js.erb +++ b/app/views/commontator/threads/_show.js.erb @@ -6,7 +6,6 @@ show_all %> - $("#commontator-thread-<%= thread.id %>").html("<%= escape_javascript( render partial: 'commontator/threads/show', formats: [ :html ], locals: { user: user, thread: thread, page: page, show_all: show_all diff --git a/app/views/layouts/devise.html.erb b/app/views/layouts/devise.html.erb index a93ca6147..2f8c2458a 100644 --- a/app/views/layouts/devise.html.erb +++ b/app/views/layouts/devise.html.erb @@ -3,12 +3,31 @@ <%= render partial: 'layouts/head' %> <%= stylesheet_link_tag 'landing' %> + + + + + + + + + + + + + <%= javascript_include_tag 'monotile/geometry' %> + <%= javascript_include_tag 'monotile/hat' %> +
+
+ +
+ <%# Github corner %> <%# from: https://github.com/tholman/github-corners %> @@ -16,7 +35,7 @@ <%# Announcements %> <% if Announcement.active_on_main.exists? %>
- <%= get_announcements().html_safe %> + <%= main_page_announcements().html_safe %>
<% end %> @@ -29,7 +48,7 @@ <% end %> <% end %>
- +
- +
<%= t('main.welcome') %>
@@ -85,10 +104,10 @@
- + - - \ No newline at end of file + + diff --git a/app/views/media/_medium.html.erb b/app/views/media/_medium.html.erb index 15096261a..d25bd05c1 100644 --- a/app/views/media/_medium.html.erb +++ b/app/views/media/_medium.html.erb @@ -62,16 +62,16 @@ title="<%= t('medium.waiting_for_tag') %>"> <% end %> - <% if !medium.containingWatchlists(current_user).empty? %> + <% if !medium.containing_watchlists(current_user).empty? %> <%= link_to '', - watchlist_path(medium.containingWatchlists(current_user).first), + watchlist_path(medium.containing_watchlists(current_user).first), class: 'fas fa-bookmark text-light me-2', style: 'text-decoration: none;', data: { toggle: 'tooltip', placement: 'bottom' }, title: t('watchlist_entry.list', - count: medium.containingWatchlists(current_user).size, - watchlists: medium.containingWatchlistsNames(current_user).join(", ")) %> + count: medium.containing_watchlists(current_user).size, + watchlists: medium.containing_watchlists_names(current_user).join(", ")) %> <% end %> <%= link_to '', add_medium_to_watchlist_path(medium), diff --git a/app/views/shared/_footer.html.erb b/app/views/shared/_footer.html.erb index 24ca5d9ba..efb373378 100644 --- a/app/views/shared/_footer.html.erb +++ b/app/views/shared/_footer.html.erb @@ -12,7 +12,7 @@ <%# Footbar %> -
+