diff --git "a/.github/ISSUE_TEMPLATE/\342\231\273\357\270\217-refactor.md" "b/.github/ISSUE_TEMPLATE/\342\231\273\357\270\217-refactor.md" new file mode 100644 index 00000000..7f82ae73 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\342\231\273\357\270\217-refactor.md" @@ -0,0 +1,39 @@ +--- +name: "♻️ Refactor" +about: Something that needs improving while not changing functionality as documented + here (https://www.ssw.com.au/rules/technical-debt/). If this doesn't look right, + choose a different type. +title: "♻️" +labels: 'Type: Refactor' +assignees: '' + +--- + + + + +Cc: + +Hi + +### Pain + + +### What code could be improved? + + +### Tasks + +- [ ] ... +- [ ] ... + +### Acceptance Criteria + + +### More Information + + +### Screenshots + + +Thanks! diff --git "a/.github/ISSUE_TEMPLATE/\342\234\250-new-feature.md" "b/.github/ISSUE_TEMPLATE/\342\234\250-new-feature.md" new file mode 100644 index 00000000..5c26f0be --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\342\234\250-new-feature.md" @@ -0,0 +1,37 @@ +--- +name: "✨ New Feature" +about: Suggest an idea. If this doesn't look right, choose a different type. +title: "✨" +labels: 'Type: Feature' +assignees: '' + +--- + + + + +Cc: + +Hi + +### Pain + + +### Suggested Solution + + +### Tasks + +- [ ] ... +- [ ] ... + +### Acceptance Criteria + + +### More Information + + +### Screenshots + + +Thanks! diff --git "a/.github/ISSUE_TEMPLATE/\360\237\220\233-bug-report.md" "b/.github/ISSUE_TEMPLATE/\360\237\220\233-bug-report.md" new file mode 100644 index 00000000..e643c379 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\360\237\220\233-bug-report.md" @@ -0,0 +1,50 @@ +--- +name: "\U0001F41B Bug Report" +about: Create a report to help us improve documented here (https://www.ssw.com.au/rules/the-right-way-to-report-bugs-and-give-feedback-suggestions). + If this doesn't look right, choose a different type. +title: "\U0001F41B " +labels: 'Type: Bug' +assignees: '' + +--- + + + + +Cc: + +Hi + +### Describe the Bug + + +### To Reproduce +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +### Expected Behavior + + +### Tasks +- [ ] Investigate +- [ ] Fix + +### Acceptance Criteria + + +### More Information + + +### Environment + - Device: [e.g. iPhone 12] + - Browser: [e.g. chrome, safari] + - OS: [e.g. iOS] + +### Screenshots + + + +Thanks! diff --git "a/.github/ISSUE_TEMPLATE/\360\237\221\267-devops.md" "b/.github/ISSUE_TEMPLATE/\360\237\221\267-devops.md" new file mode 100644 index 00000000..8bc851bb --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\360\237\221\267-devops.md" @@ -0,0 +1,39 @@ +--- +name: "\U0001F477 DevOps" +about: CI/CD and other DevOps concerns e.g. Updating the build, code analysis, test, + deploy, application monitoring etc. If this doesn't look right, choose a different + type. +title: "\U0001F477" +labels: 'Type: DevOps' +assignees: '' + +--- + + + + +Cc: + +Hi + +### Pain + + +### Suggested Solution + + +### Tasks + +- [ ] ... +- [ ] ... + +### Acceptance Criteria + + +### More Information + + +### Screenshots + + +Thanks! diff --git "a/.github/ISSUE_TEMPLATE/\360\237\223\235-documentation.md" "b/.github/ISSUE_TEMPLATE/\360\237\223\235-documentation.md" new file mode 100644 index 00000000..73bd446b --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\360\237\223\235-documentation.md" @@ -0,0 +1,39 @@ +--- +name: "\U0001F4DD Documentation" +about: Updating documentation (e.g. README, Wiki, Guides etc.) as documented here + (https://www.ssw.com.au/rules/awesome-documentation/) If this doesn't look right, + choose a different type. +title: "\U0001F4DD" +labels: 'Type: Documentation' +assignees: '' + +--- + + + + +Cc: + +Hi + +### Pain + + +### Suggested Documentation + + +### Tasks + +- [ ] ... +- [ ] ... + +### Acceptance Criteria + + +### More Information + + +### Screenshots + + +Thanks! diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..6fa0b26d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,33 @@ + + +## Description + + +## Related Issue + + + + + +## Motivation and Context + + +## How Has This Been Tested? + + + + +## Screenshots (if appropriate): + +## Types of changes + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) + +## Checklist: + + +- [ ] My code follows the code style of this project. +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 00000000..2a3f734f --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,131 @@ +name: Verify +on: [push, pull_request] + +jobs: + linters: + name: Linters + permissions: write-all + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - run: | + git fetch --no-tags --unshallow --prune origin +refs/heads/*:refs/remotes/origin/* + + - name: Setup Ruby and install gems + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Run linters + run: | + bundle exec pronto run -c origin/master --exit-code + # bundle exec stylelint + # bundle exec prettier + # bundle exec eslint --fix $(git diff --name-only HEAD | xargs) + env: + PRONTO_PULL_REQUEST_ID: ${{ github.event.pull_request.number }} + PRONTO_GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # security_checks: + # name: Security checks + # runs-on: ubuntu-latest + # steps: + # - name: Checkout code + # uses: actions/checkout@v2 + # - run: | + # git fetch --no-tags --prune origin +refs/heads/*:refs/remotes/origin/* + + # - name: Setup Ruby and install gems + # uses: ruby/setup-ruby@v1 + # with: + # bundler-cache: true + + # - name: Run security checks + # run: | + # bundle exec bundler-audit --update + # bundle exec brakeman -q -w2 + tests: + name: Tests + runs-on: ubuntu-latest + services: + mysql: + image: mysql:5.7 + env: + MYSQL_HOST: 127.0.0.1 + MYSQL_USER: root + MYSQL_PASSWORD: root + MYSQL_DATABASE: ssid_test + MYSQL_ROOT_PASSWORD: root + ports: + - "3306:3306" + # options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup Ruby and install gems + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + # - name: Setup Node + # uses: actions/setup-node@v2 + # with: + # node-version: 16 + # cache: yarn + + - name: Install packages + run: | + yarn install --pure-lockfile + - name: Setup test database + env: + RAILS_ENV: test + DATABASE_PORT: 3306 + DB_HOST: 127.0.0.1 + DB_CONNECTION: mysql + DB_DATABASE: ssid_test + DB_USER: root + DB_PASSWORD: root + run: | + sudo /etc/init.d/mysql start + mysql -e 'CREATE DATABASE ssid_test;' -u root -proot + bundle exec rails db:create + bundle exec rails db:migrate RAILS_ENV=test + bundle exec rails db:seed + + - name: Install wkhtmltopdf + run: | + sudo apt-get update + sudo apt-get install -y wkhtmltopdf + + - name: Install chrome + id: setup-chrome + uses: browser-actions/setup-chrome@v1 + with: + chrome-version: stable + + - name: Show chrome version + run: | + echo Installed chromium version: ${{ steps.setup-chrome.outputs.chrome-version }} ${{ steps.setup-chrome.outputs.chrome-path }} --version + + - name: Setup chromedriver + uses: nanasess/setup-chromedriver@v2 + + - name: Run local server for testing + run: | + bundle exec rails server -d -p 3000 -e test + sleep 5 + - name: Run tests + run: | + bundle exec rspec spec/api_requests/ + bundle exec rspec spec/routes + # bundle exec rspec spec/landing_page_spec.rb + + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: "./coverage/lcov.info" \ No newline at end of file diff --git a/.gitignore b/.gitignore index b38e36a1..23596b07 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ /yarn-error.log yarn-debug.log* .yarn-integrity +*.yml diff --git a/.pronto.yml b/.pronto.yml new file mode 100644 index 00000000..3b1482a1 --- /dev/null +++ b/.pronto.yml @@ -0,0 +1,18 @@ +all: + exclude: + - 'spec/**/*' +# exclude files for single runner +eslint: + exclude: + - 'app/assets/**/*' + - 'coverage/**/*' +github: + slug: prontolabs/pronto + access_token: ${{ secrets.GITHUB_TOKEN }} + api_endpoint: https://api.github.com/ + web_endpoint: https://github.com/ +max_warnings: 150 +warnings_per_review: 30 +verbose: false +runners: [rubocop, eslint] # only listed runners will be executed +skip_runners: [reek] # all, except listed runners will be executed \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..35706250 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,346 @@ +inherit_from: .rubocop_todo.yml + +require: + - rubocop-rails + - rubocop-rspec +AllCops: + Exclude: + - 'db/**/*' + - vendor/bundle/**/* + +Gemspec/DeprecatedAttributeAssignment: # new in 1.30 + Enabled: true +Gemspec/DevelopmentDependencies: # new in 1.44 + Enabled: true +Gemspec/RequireMFA: # new in 1.23 + Enabled: true +Layout/LineContinuationLeadingSpace: # new in 1.31 + Enabled: true +Layout/LineContinuationSpacing: # new in 1.31 + Enabled: true +Layout/LineEndStringConcatenationIndentation: # new in 1.18 + Enabled: true +Layout/SpaceBeforeBrackets: # new in 1.7 + Enabled: true +Lint/AmbiguousAssignment: # new in 1.7 + Enabled: true +Lint/AmbiguousOperatorPrecedence: # new in 1.21 + Enabled: true +Lint/AmbiguousRange: # new in 1.19 + Enabled: true +Lint/ConstantOverwrittenInRescue: # new in 1.31 + Enabled: true +Lint/DeprecatedConstants: # new in 1.8 + Enabled: true +Lint/DuplicateBranch: # new in 1.3 + Enabled: true +Lint/DuplicateMagicComment: # new in 1.37 + Enabled: true +Lint/DuplicateMatchPattern: # new in 1.50 + Enabled: true +Lint/DuplicateRegexpCharacterClassElement: # new in 1.1 + Enabled: true +Lint/EmptyBlock: # new in 1.1 + Enabled: true +Lint/EmptyClass: # new in 1.3 + Enabled: true +Lint/EmptyInPattern: # new in 1.16 + Enabled: true +Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21 + Enabled: true +Lint/LambdaWithoutLiteralBlock: # new in 1.8 + Enabled: true +Lint/NoReturnInBeginEndBlocks: # new in 1.2 + Enabled: true +Lint/NonAtomicFileOperation: # new in 1.31 + Enabled: true +Lint/NumberedParameterAssignment: # new in 1.9 + Enabled: true +Lint/OrAssignmentToConstant: # new in 1.9 + Enabled: true +Lint/RedundantDirGlobSort: # new in 1.8 + Enabled: true +Lint/RefinementImportMethods: # new in 1.27 + Enabled: true +Lint/RequireRangeParentheses: # new in 1.32 + Enabled: true +Lint/RequireRelativeSelfPath: # new in 1.22 + Enabled: true +Lint/SymbolConversion: # new in 1.9 + Enabled: true +Lint/ToEnumArguments: # new in 1.1 + Enabled: true +Lint/TripleQuotes: # new in 1.9 + Enabled: true +Lint/UnexpectedBlockArity: # new in 1.5 + Enabled: true +Lint/UnmodifiedReduceAccumulator: # new in 1.1 + Enabled: true +Lint/UselessRescue: # new in 1.43 + Enabled: true +Lint/UselessRuby2Keywords: # new in 1.23 + Enabled: true +Metrics/CollectionLiteralLength: # new in 1.47 + Enabled: true +Naming/BlockForwarding: # new in 1.24 + Enabled: true +Security/CompoundHash: # new in 1.28 + Enabled: true +Security/IoMethods: # new in 1.22 + Enabled: true +Style/ArgumentsForwarding: # new in 1.1 + Enabled: true +Style/ArrayIntersect: # new in 1.40 + Enabled: true +Style/CollectionCompact: # new in 1.2 + Enabled: true +Style/ComparableClamp: # new in 1.44 + Enabled: true +Style/ConcatArrayLiterals: # new in 1.41 + Enabled: true +Style/DataInheritance: # new in 1.49 + Enabled: true +Style/DirEmpty: # new in 1.48 + Enabled: true +Style/DocumentDynamicEvalDefinition: # new in 1.1 + Enabled: true +Style/EmptyHeredoc: # new in 1.32 + Enabled: true +Style/EndlessMethod: # new in 1.8 + Enabled: true +Style/EnvHome: # new in 1.29 + Enabled: true +Style/FetchEnvVar: # new in 1.28 + Enabled: true +Style/FileEmpty: # new in 1.48 + Enabled: true +Style/FileRead: # new in 1.24 + Enabled: true +Style/FileWrite: # new in 1.24 + Enabled: true +Style/HashConversion: # new in 1.10 + Enabled: true +Style/HashExcept: # new in 1.7 + Enabled: true +Style/IfWithBooleanLiteralBranches: # new in 1.9 + Enabled: true +Style/InPatternThen: # new in 1.16 + Enabled: true +Style/MagicCommentFormat: # new in 1.35 + Enabled: true +Style/MapCompactWithConditionalBlock: # new in 1.30 + Enabled: true +Style/MapToHash: # new in 1.24 + Enabled: true +Style/MapToSet: # new in 1.42 + Enabled: true +Style/MinMaxComparison: # new in 1.42 + Enabled: true +Style/MultilineInPatternThen: # new in 1.16 + Enabled: true +Style/NegatedIfElseCondition: # new in 1.2 + Enabled: true +Style/NestedFileDirname: # new in 1.26 + Enabled: true +Style/NilLambda: # new in 1.3 + Enabled: true +Style/NumberedParameters: # new in 1.22 + Enabled: true +Style/NumberedParametersLimit: # new in 1.22 + Enabled: true +Style/ObjectThen: # new in 1.28 + Enabled: true +Style/OpenStructUse: # new in 1.23 + Enabled: true +Style/OperatorMethodCall: # new in 1.37 + Enabled: true +Style/QuotedSymbols: # new in 1.16 + Enabled: true +Style/RedundantArgument: # new in 1.4 + Enabled: true +Style/RedundantConstantBase: # new in 1.40 + Enabled: true +Style/RedundantDoubleSplatHashBraces: # new in 1.41 + Enabled: true +Style/RedundantEach: # new in 1.38 + Enabled: true +Style/RedundantHeredocDelimiterQuotes: # new in 1.45 + Enabled: true +Style/RedundantInitialize: # new in 1.27 + Enabled: true +Style/RedundantLineContinuation: # new in 1.49 + Enabled: true +Style/RedundantSelfAssignmentBranch: # new in 1.19 + Enabled: true +Style/RedundantStringEscape: # new in 1.37 + Enabled: true +Style/SelectByRegexp: # new in 1.22 + Enabled: true +Style/StringChars: # new in 1.12 + Enabled: true +Style/SwapValues: # new in 1.1 + Enabled: true +Rails/ActionControllerFlashBeforeRender: # new in 2.16 + Enabled: true +Rails/ActionControllerTestCase: # new in 2.14 + Enabled: true +Rails/ActionOrder: # new in 2.17 + Enabled: true +Rails/ActiveRecordCallbacksOrder: # new in 2.7 + Enabled: true +Rails/ActiveSupportOnLoad: # new in 2.16 + Enabled: true +Rails/AddColumnIndex: # new in 2.11 + Enabled: true +Rails/AfterCommitOverride: # new in 2.8 + Enabled: true +Rails/AttributeDefaultBlockValue: # new in 2.9 + Enabled: true +Rails/CompactBlank: # new in 2.13 + Enabled: true +Rails/DeprecatedActiveModelErrorsMethods: # new in 2.14 + Enabled: true +Rails/DotSeparatedKeys: # new in 2.15 + Enabled: true +Rails/DuplicateAssociation: # new in 2.14 + Enabled: true +Rails/DuplicateScope: # new in 2.14 + Enabled: true +Rails/DurationArithmetic: # new in 2.13 + Enabled: true +Rails/EagerEvaluationLogMessage: # new in 2.11 + Enabled: true +Rails/ExpandedDateRange: # new in 2.11 + Enabled: true +Rails/FindById: # new in 2.7 + Enabled: true +Rails/FreezeTime: # new in 2.16 + Enabled: true +Rails/I18nLazyLookup: # new in 2.14 + Enabled: true +Rails/I18nLocaleAssignment: # new in 2.11 + Enabled: true +Rails/I18nLocaleTexts: # new in 2.14 + Enabled: true +Rails/IgnoredColumnsAssignment: # new in 2.17 + Enabled: true +Rails/Inquiry: # new in 2.7 + Enabled: true +Rails/MailerName: # new in 2.7 + Enabled: true +Rails/MatchRoute: # new in 2.7 + Enabled: true +Rails/MigrationClassName: # new in 2.14 + Enabled: true +Rails/NegateInclude: # new in 2.7 + Enabled: true +Rails/Pluck: # new in 2.7 + Enabled: true +Rails/PluckInWhere: # new in 2.7 + Enabled: true +Rails/RedundantPresenceValidationOnBelongsTo: # new in 2.13 + Enabled: true +Rails/RedundantTravelBack: # new in 2.12 + Enabled: true +Rails/RenderInline: # new in 2.7 + Enabled: true +Rails/RenderPlainText: # new in 2.7 + Enabled: true +Rails/ResponseParsedBody: # new in 2.18 + Enabled: true +Rails/RootJoinChain: # new in 2.13 + Enabled: true +Rails/RootPathnameMethods: # new in 2.16 + Enabled: true +Rails/RootPublicPath: # new in 2.15 + Enabled: true +Rails/ShortI18n: # new in 2.7 + Enabled: true +Rails/SquishedSQLHeredocs: # new in 2.8 + Enabled: true +Rails/StripHeredoc: # new in 2.15 + Enabled: true +Rails/ThreeStateBooleanColumn: # new in 2.19 + Enabled: true +Rails/TimeZoneAssignment: # new in 2.10 + Enabled: true +Rails/ToFormattedS: # new in 2.15 + Enabled: true +Rails/ToSWithArgument: # new in 2.16 + Enabled: true +Rails/TopLevelHashWithIndifferentAccess: # new in 2.16 + Enabled: true +Rails/TransactionExitStatement: # new in 2.14 + Enabled: true +Rails/UnusedIgnoredColumns: # new in 2.11 + Enabled: true +Rails/WhereEquals: # new in 2.9 + Enabled: true +Rails/WhereExists: # new in 2.7 + Enabled: true +Rails/WhereMissing: # new in 2.16 + Enabled: true +Rails/WhereNot: # new in 2.8 + Enabled: true +Rails/WhereNotWithMultipleConditions: # new in 2.17 + Enabled: true +Capybara/MatchStyle: # new in 2.17 + Enabled: true +Capybara/NegationMatcher: # new in 2.14 + Enabled: true +Capybara/SpecificActions: # new in 2.14 + Enabled: true +Capybara/SpecificFinders: # new in 2.13 + Enabled: true +Capybara/SpecificMatcher: # new in 2.12 + Enabled: true +RSpec/BeEmpty: # new in 2.20 + Enabled: true +RSpec/BeEq: # new in 2.9.0 + Enabled: true +RSpec/BeNil: # new in 2.9.0 + Enabled: true +RSpec/ChangeByZero: # new in 2.11 + Enabled: true +RSpec/ContainExactly: # new in 2.19 + Enabled: true +RSpec/DuplicatedMetadata: # new in 2.16 + Enabled: true +RSpec/ExcessiveDocstringSpacing: # new in 2.5 + Enabled: true +RSpec/IdenticalEqualityAssertion: # new in 2.4 + Enabled: true +RSpec/IndexedLet: # new in 2.20 + Enabled: true +RSpec/MatchArray: # new in 2.19 + Enabled: true +RSpec/NoExpectationExample: # new in 2.13 + Enabled: true +RSpec/PendingWithoutReason: # new in 2.16 + Enabled: true +RSpec/RedundantAround: # new in 2.19 + Enabled: true +RSpec/SkipBlockInsideExample: # new in 2.19 + Enabled: true +RSpec/SortMetadata: # new in 2.14 + Enabled: true +RSpec/SubjectDeclaration: # new in 2.5 + Enabled: true +RSpec/VerifiedDoubleReference: # new in 2.10.0 + Enabled: true +RSpec/FactoryBot/ConsistentParenthesesStyle: # new in 2.14 + Enabled: true +RSpec/FactoryBot/FactoryNameStyle: # new in 2.16 + Enabled: true +RSpec/FactoryBot/SyntaxMethods: # new in 2.7 + Enabled: true +RSpec/Rails/AvoidSetupHook: # new in 2.4 + Enabled: true +RSpec/Rails/HaveHttpStatus: # new in 2.12 + Enabled: true +RSpec/Rails/InferredSpecType: # new in 2.14 + Enabled: true +RSpec/Rails/MinitestAssertions: # new in 2.17 + Enabled: true +RSpec/Rails/TravelAround: # new in 2.19 + Enabled: true \ No newline at end of file diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 00000000..e69de29b diff --git a/.simplecov b/.simplecov new file mode 100644 index 00000000..33905ba4 --- /dev/null +++ b/.simplecov @@ -0,0 +1,24 @@ +require 'simplecov-lcov' + +SimpleCov::Formatter::LcovFormatter.config do |c| + c.report_with_single_file = true + c.single_report_path = 'coverage/lcov.info' +end +SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new( + [ + SimpleCov::Formatter::HTMLFormatter, + SimpleCov::Formatter::LcovFormatter, + ] +) + +SimpleCov.start('rails') do + add_filter '/test/' + add_filter '/config/' + add_filter '/vendor/' + + add_group 'Controllers', 'app/controllers' + add_group 'Models', 'app/models' + add_group 'Helpers', 'app/helpers' + add_group 'Mailers', 'app/mailers' + add_group 'Services', 'app/services' +end diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..25f0abce --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +FROM ruby:2.6.6 + +# install java & set path +RUN apt-get update +RUN apt-get -y install -y sudo + +RUN apt-get install -y openjdk-11-jdk +RUN sudo update-alternatives --config java + +ENV JAVA_HOME /usr/lib/jvm/java-11-openjdk-amd64/ +RUN export JAVA_HOME +RUN javac -version + +# install ant +RUN apt update +RUN apt -y install ant +# RUN ant -version # Causes issues with docker build + +# install antlr +WORKDIR /usr/local/lib +RUN mkdir -p /usr/java/lib +RUN wget http://www.antlr.org/download/antlr-4.8-complete.jar -P /usr/local/lib +RUN echo '#!/bin/bash\njava -jar /usr/local/lib/antlr-4.8-complete.jar' > /usr/bin/antlr4 +RUN chmod +x /usr/bin/antlr4 +RUN echo '#!/bin/bash\njava org.antlr.v4.gui.TestRig' > /usr/bin/grun +RUN chmod +x /usr/bin/grun +ENV CLASSPATH .:/usr/local/lib/antlr-4.8-complete.jar:$CLASSPATH +WORKDIR / + +# install mysql client +RUN apt update +RUN apt-get install default-mysql-client -y + +# install rails dependencies +RUN apt-get clean all +RUN apt-get update -qq +RUN apt-get install -y build-essential libpq-dev \ + curl gnupg2 apt-utils default-libmysqlclient-dev git libcurl3-dev cmake \ + libssl-dev pkg-config openssl imagemagick file nodejs yarn + +RUN mkdir /ssid +WORKDIR /ssid + +# Adding gems +COPY Gemfile Gemfile +COPY Gemfile.lock Gemfile.lock +RUN gem install bundler -v 2.4.18 +RUN bundle install + +COPY . /ssid + +# Add a script to be executed every time the container starts. +COPY entrypoint.sh /usr/bin/ +RUN chmod +x /usr/bin/entrypoint.sh +ENTRYPOINT ["entrypoint.sh"] \ No newline at end of file diff --git a/Gemfile b/Gemfile index bd404264..1040b36d 100644 --- a/Gemfile +++ b/Gemfile @@ -28,6 +28,9 @@ gem 'rubyzip', '~> 2.3' gem 'sprockets-rails', :require => 'sprockets/railtie' +# handles user authentication and authorization +gem 'devise', '~> 4.7' + # extends :File methods # gem 'win32-file', '~> 0.8.2' # gem 'ptools', '~> 1.3', '>= 1.3.7' @@ -60,6 +63,30 @@ gem 'bootsnap', '>= 1.4.2', require: false group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] + + # Testing + gem 'capybara', '>= 2.15' + gem 'rspec-rails', '~> 4.0.2' + gem 'selenium-webdriver' + gem 'webdrivers' + + # Linter for Ruby code + gem 'pronto' + gem 'pronto-eslint', require: false + gem 'pronto-rubocop', require: false + + gem 'prettier_print' + gem 'rubocop-changes' + gem 'rubocop-rails', require: false + gem 'rubocop-rake', require: false + gem 'rubocop-rspec', require: false + # Security scanners + gem 'brakeman' + gem 'bundle-audit' + + # Coverage + gem 'simplecov', require: false + gem 'simplecov-lcov', require: false end group :development do @@ -67,13 +94,13 @@ group :development do gem 'web-console', '>= 3.3.0' end -group :test do - # Adds support for Capybara system testing and selenium driver - gem 'capybara', '>= 2.15' - gem 'selenium-webdriver' - # Easy installation and use of web drivers to run system tests with browsers - gem 'webdrivers' -end - # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] + +gem "dotenv-rails" +gem 'omniauth' +gem 'omniauth-google-oauth2' +gem "omniauth-rails_csrf_protection" + +gem 'passenger' +gem 'pdfkit' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 49ec71e4..e2374d8a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -58,6 +58,7 @@ GEM zeitwerk (~> 2.2, >= 2.2.2) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) + ast (2.4.2) autoprefixer-rails (10.3.3.0) execjs (~> 2) bcrypt (3.1.16) @@ -68,7 +69,13 @@ GEM autoprefixer-rails (>= 9.1.0) popper_js (>= 2.9.3, < 3) sassc-rails (>= 2.0.0) + brakeman (5.4.1) builder (3.2.4) + bundle-audit (0.1.0) + bundler-audit + bundler-audit (0.9.1) + bundler (>= 1.2.0, < 3) + thor (~> 1.0) byebug (11.1.3) capybara (3.35.3) addressable @@ -89,11 +96,39 @@ GEM coffee-script-source (1.12.2) concurrent-ruby (1.1.10) crass (1.0.6) + devise (4.8.1) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + diff-lcs (1.5.0) + docile (1.4.0) + dotenv (2.8.1) + dotenv-rails (2.8.1) + dotenv (= 2.8.1) + railties (>= 3.2) erubi (1.10.0) + eslintrb (2.1.0) + execjs + multi_json (>= 1.3) + rake execjs (2.8.1) + faraday (2.7.4) + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-net_http (3.0.2) ffi (1.15.0) + git_diff_parser (3.2.0) + gitlab (4.19.0) + httparty (~> 0.20) + terminal-table (>= 1.5.1) globalid (1.0.1) activesupport (>= 5.0) + hashie (5.0.0) + httparty (0.21.0) + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) i18n (1.12.0) concurrent-ruby (~> 1.0) jbuilder (2.11.2) @@ -102,6 +137,8 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) + json (2.6.3) + jwt (2.7.0) loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -113,6 +150,8 @@ GEM mini_portile2 (2.8.1) minitest (5.17.0) msgpack (1.4.2) + multi_json (1.15.0) + multi_xml (0.6.0) mysql2 (0.5.3) nio4r (2.5.8) nokogiri (1.13.10) @@ -120,12 +159,63 @@ GEM racc (~> 1.4) nokogiri (1.13.10-x86_64-linux) racc (~> 1.4) + oauth2 (2.0.9) + faraday (>= 0.17.3, < 3.0) + jwt (>= 1.0, < 3.0) + multi_xml (~> 0.5) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0) + version_gem (~> 1.1) + octokit (4.25.1) + faraday (>= 1, < 3) + sawyer (~> 0.9) + omniauth (2.1.1) + hashie (>= 3.4.6) + rack (>= 2.2.3) + rack-protection + omniauth-google-oauth2 (1.1.1) + jwt (>= 2.0) + oauth2 (~> 2.0.6) + omniauth (~> 2.0) + omniauth-oauth2 (~> 1.8.0) + omniauth-oauth2 (1.8.0) + oauth2 (>= 1.4, < 3) + omniauth (~> 2.0) + omniauth-rails_csrf_protection (1.0.1) + actionpack (>= 4.2) + omniauth (~> 2.0) + orm_adapter (0.5.0) + parallel (1.23.0) + parser (3.2.2.3) + ast (~> 2.4.1) + racc + passenger (6.0.18) + rack + rake (>= 0.8.1) + pdfkit (0.8.7.3) popper_js (2.9.3) + prettier_print (1.2.1) + pronto (0.11.1) + gitlab (>= 4.4.0, < 5.0) + httparty (>= 0.13.7, < 1.0) + octokit (>= 4.7.0, < 7.0) + rainbow (>= 2.2, < 4.0) + rexml (>= 3.2.5, < 4.0) + rugged (>= 0.23.0, < 2.0) + thor (>= 0.20.3, < 2.0) + pronto-eslint (0.11.1) + eslintrb (~> 2.0, >= 2.0.0) + pronto (~> 0.11.0) + pronto-rubocop (0.11.5) + pronto (~> 0.11.0) + rubocop (>= 0.63.1, < 2.0) public_suffix (4.0.6) puma (4.3.12) nio4r (~> 2.0) racc (1.6.2) - rack (2.2.6.2) + rack (2.2.6.4) + rack-protection (3.0.6) + rack rack-test (1.1.0) rack (>= 1.0, < 3) rails (6.0.3.7) @@ -154,9 +244,60 @@ GEM method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) + rainbow (3.1.1) rake (13.0.3) regexp_parser (2.1.1) + responders (3.0.1) + actionpack (>= 5.0) + railties (>= 5.0) + rexml (3.2.6) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.6) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-rails (4.0.2) + actionpack (>= 4.2) + activesupport (>= 4.2) + railties (>= 4.2) + rspec-core (~> 3.10) + rspec-expectations (~> 3.10) + rspec-mocks (~> 3.10) + rspec-support (~> 3.10) + rspec-support (3.12.1) + rubocop (1.50.2) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.2.0.0) + 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) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.29.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.18.0) + rubocop (~> 1.41) + rubocop-changes (0.8.1) + git_diff_parser (~> 3.2) + rubocop (>= 1.0) + rubocop-rails (2.19.1) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-rake (0.6.0) + rubocop (~> 1.0) + rubocop-rspec (2.20.0) + rubocop (~> 1.33) + rubocop-capybara (~> 2.17) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) rubyzip (2.3.0) + rugged (1.7.1) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) @@ -167,9 +308,22 @@ GEM sprockets (> 3.0) sprockets-rails tilt + sawyer (0.9.2) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) selenium-webdriver (3.142.7) childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov-lcov (0.8.0) + simplecov_json_formatter (0.1.4) + snaky_hash (2.0.1) + hashie + version_gem (~> 1.1, >= 1.1.1) sprockets (4.0.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -177,6 +331,8 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) thor (1.1.0) thread_safe (0.3.6) tilt (2.0.10) @@ -187,6 +343,10 @@ GEM thread_safe (~> 0.1) uglifier (4.2.0) execjs (>= 0.3.0, < 3) + unicode-display_width (2.4.2) + version_gem (1.1.1) + warden (1.2.9) + rack (>= 2.0.9) web-console (4.1.0) actionview (>= 6.0.0) activemodel (>= 6.0.0) @@ -215,17 +375,37 @@ DEPENDENCIES bcrypt (~> 3.1.7) bootsnap (>= 1.4.2) bootstrap (~> 5.1.3) + brakeman + bundle-audit byebug capybara (>= 2.15) coffee-rails (~> 5.0) + devise (~> 4.7) + dotenv-rails jbuilder (~> 2.7) jquery-rails (~> 4.4) mysql2 (~> 0.5.3) + omniauth + omniauth-google-oauth2 + omniauth-rails_csrf_protection + passenger + pdfkit + prettier_print + pronto + pronto-eslint + pronto-rubocop puma (~> 4.3) rails (~> 6.0.3, >= 6.0.3.2) + rspec-rails (~> 4.0.2) + rubocop-changes + rubocop-rails + rubocop-rake + rubocop-rspec rubyzip (~> 2.3) sass-rails (~> 6.0) selenium-webdriver + simplecov + simplecov-lcov sprockets-rails turbolinks (~> 5) tzinfo-data @@ -239,4 +419,4 @@ RUBY VERSION ruby 2.6.6p146 BUNDLED WITH - 2.2.17 + 2.4.18 diff --git a/README-v-Beginner.md b/README-v-Beginner.md index 6512be8f..e3c34959 100644 --- a/README-v-Beginner.md +++ b/README-v-Beginner.md @@ -4,7 +4,7 @@ **Guide in One Paragraph**: This is an installation guide for those who have limited knowledge of Linux. This guide assumes only limited fluency with `bash`, `tmux`, and `conda`. This guide wishes to save your time by telling you the trick in critical steps. -Last update: 18th March 2021 (pre-dockerized configuration) +Last update: 18th September 2023 (pre-dockerized configuration) ## Package Installation @@ -17,6 +17,16 @@ Make sure to install all packages in the version as **exactly** specified in the Yisong's config: a standard Ubuntu 18.04 version Linux. +## WSL Support + +For Windows Users who are not planning to do a dual-boot on their computer, WSL works just as well for most of the SSID operations. You can refer to the following link on how to setup Ubuntu with WSL on your computer. + +https://learn.microsoft.com/en-us/windows/wsl/install + +Our recommended method is to activate WSL and then install an Ubuntu distribution from the Microsoft Store. Current SSID supports Ubuntu 22.04 (LTS). + +For those who are using WSL, you might find that some of the commands provided in the below tutorials are rather Linux specific (e.g. `sudo systemctl start `). On our case, if you are using WSL, you might find that the syntax might be different (e.g. `sudo service start`). Do adjust accordingly. + ### Java 11 No difficulty installing `Java 11`. @@ -29,6 +39,16 @@ https://www.digitalocean.com/community/tutorials/how-to-install-java-with-apt-on ### Ruby v2.6.6. ; bundler v2.1.4 +(Newer Update): If you are using Ubuntu 22.04 (LTS), you may benefit from following the steps specified in the following link: + +``` +https://www.digitalocean.com/community/tutorials/how-to-install-ruby-on-rails-with-rbenv-on-ubuntu-22-04 +``` + +Complete the installation up to the following step: `echo "gem: --no-document" > ~/.gemrc`, then proceed to the "installing bundler" section. + +(Older Update: Ubuntu 18.04) + I had a hard time installing `Ruby v2.6.6`., which was the current version at the time of writing this installation guide. I forgot my specific steps, but these tips are useful: @@ -63,6 +83,8 @@ A successful installation may follow these steps: https://rubygems.org/gems/bundler/versions/2.1.4 ``` +### Installing Bundler + Make sure to follow these commands: ``` @@ -90,6 +112,13 @@ The last package actually takes a significant time to properly install and confi https://phoenixnap.com/kb/how-to-install-mysql-on-ubuntu-18-04 ``` +Alternatively, you may also want to refer to the following instruction: + +https://www.digitalocean.com/community/tutorials/how-to-install-mysql-on-ubuntu-22-04 + +This will provide a better instruction for Ubuntu 22.04 (LTS) users. + + Step 2 in this tutorial is very critical for Linux users. 🎉🎉🎉 Congratulations! You have successfully installed all packages! @@ -109,6 +138,61 @@ Please `mysql -u root -p` to try to login into `root` to see if the password is 🎉🎉🎉 All done! +## TroubleShooting + +### libmysqlclient.so error + +This error occurs when doing `bundle install`, specifically with installing the `rugged` gem. + +Exact Error Message: + +``` +LoadError: libmysqlclient.so.21: cannot open shared object file: No such file or directory - /root/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/mysql2-0.5.3/lib/mysql2/mysql2.so +/mnt/c/Users/Jason C/Code/SSID/config/application.rb:7:in `' +/mnt/c/Users/Jason C/Code/SSID/rakefile:4:in `require_relative' +/mnt/c/Users/Jason C/Code/SSID/rakefile:4:in `' +(See full trace by running task with --trace) +``` + +Suggested solution: + +Install the `libmysqlclient.so` MySQL Client Library by running the following command: + +``` +sudo apt-get install libmysqlclient-dev +``` + +Then re-run `bundle install`. This should do the trick. + + +### MySQL: Error 1045 + +You might encounter the following error when using MySQL: + +``` +ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES) +``` + +In this case, most likely you have entered a wrong password that is different from what you set during the MySQL installation for the root user. + +If you have forgotten the password, you may follow the instruction below: + +### MySQL: Update Password + +You can refer to the following instruction to change the root password: + +``` +https://docs.rackspace.com/docs/reset-a-mysql-root-password +``` + +Alternatively, if everything else fails, you can remove MySQL DB and install it again, should you don't have any crucial data that is stored in MySQL (noting that you have recently installed...) + +You can refer to the following guide: + +``` +https://linuxgenie.net/uninstall-mysql-from-ubuntu/ +``` + ## Final Words If you have a problem installing, don't hesitate to ask our team, Yisong and Riyas. One response of ours may save you hours! diff --git a/README.md b/README.md index e360b403..3732873f 100644 --- a/README.md +++ b/README.md @@ -25,18 +25,18 @@ SSID works with lexers based on [ANTLR4 Grammars](https://github.com/antlr/gramm ## Setup and Configuration -Before following the below instructions, please ensure that you have met all the prerequesties listed +Before following the below instructions, please ensure that you have met all the prerequesties listed below. Alternatively, you can also setup the application via docker by clicking on the docker setup documentation all the way below. 1. Clone SSID's source code onto your computer
git clone https://github.com/WING-NUS/SSID.git
2. Go to *config/database.yml* and modify the file by changing the username and password fields with your MySQL database settings (Please do it for all the 3 listed databases in the file) -3. Now, go to *config/envionments/* and add the respective line to the respective file(s) - - Under *config/envionments/development.rb* & *config/envionments/test.rb*, add the below line: +3. Now, go to *config/environments/* and add the respective line to the respective file(s) + - Under *config/environments/development.rb* & *config/environments/test.rb*, add the below line:
config.eager_load = false
- - Under *config/envionments/production.rb* add the below line: + - Under *config/environments/production.rb* add the below line:
config.eager_load = true
4. Open your terminal and navigate to the code directory. Run bundler to install the necessary gems (including rails) from the root directory of SSID: @@ -67,6 +67,7 @@ Before following the below instructions, please ensure that you have met all the ## Site Map +- [Setting up using Docker] (doc/docker.md) - [Adding support for new language in SSID](doc/add_support_for_new_language.md) - [Deploying SSID app on a Linux/Unix production server](doc/deploying_rails_on_linux.md) - [Guide for semestral clearing of courses and submissions](doc/semestral_clearing_guide.md) diff --git a/Tools/Anonymization/Output/Student-01.py b/Tools/Anonymization/Output/Student-01.py new file mode 100644 index 00000000..153ae420 --- /dev/null +++ b/Tools/Anonymization/Output/Student-01.py @@ -0,0 +1,28 @@ + +from collections import Counter +def is_anagram(s1, s2): + c=Counter(list(s1.lower())) + d=Counter(list(s2.lower())) + if c==d: + return True + return False + +def fac(x): + f=1 + for i in range(1,x+1): + f=f*i + return f + +def binom_coeff(n, k): + result=fac(n) return result + +def fact(x): + result=1 + if x>0: + result=x*fact(x-1) + x=x-1 + return result + +def binom_coeff_recur(n, k): + result1=fact(n)/((fact(k))*(fact(n-k))) + return int(result1) diff --git a/Tools/Anonymization/Output/Student-02.py b/Tools/Anonymization/Output/Student-02.py new file mode 100644 index 00000000..00b612bd --- /dev/null +++ b/Tools/Anonymization/Output/Student-02.py @@ -0,0 +1,36 @@ + +def is_anagram(s1, s2): + + if len(s1) != len(s2): + return False + s1 = s1.lower() + s2 = s2.lower() + + s1List = [] + for letter in s1: + s1List.append(letter) + + for letter in s2: + if letter in s1List: + s1List.remove(letter) + + if len(s1List)==0: + return True + else: + return False + +def binom_coeff(n, k): + x = n - k + return ((factorial(n)) +def factorial(y): + fact = 1 + for i in range(1,y+1): + fact = fact*i + return fact + +def binom_coeff_recur(n, k): + if k == 0 or n == k: + return 1 + else: + return int(binom_coeff_recur(n-1,k-1)) + int(binom_coeff_recur(n-1,k)) + \ No newline at end of file diff --git a/Tools/Anonymization/Output/sample.c b/Tools/Anonymization/Output/sample.c new file mode 100644 index 00000000..bb1f7738 --- /dev/null +++ b/Tools/Anonymization/Output/sample.c @@ -0,0 +1,29 @@ + + + + + + + double calc_percentage_accuracy +(long num_of_testing_samples, long num_read_wrong) +{ + return + ((num_of_testing_samples - + (double)num_read_wrong)/num_of_testing_samples)*100.00; +} + +int main() +{ + const long num_of_training_samples = cs1010_read_long(); + + struct sample_holder *training_samples = calloc(num_of_training_samples, + sizeof(struct sample_holder)); + + read_input(num_of_training_samples, training_samples); + const long num_of_testing_samples = cs1010_read_long(); + + struct sample_holder *testing_samples = calloc(num_of_testing_samples, + sizeof(struct sample_holder));; + + read_input(num_of_testing_samples, testing_samples); +} \ No newline at end of file diff --git a/Tools/Anonymization/main.py b/Tools/Anonymization/main.py index 8de6710a..c6e1dbaa 100644 --- a/Tools/Anonymization/main.py +++ b/Tools/Anonymization/main.py @@ -137,6 +137,7 @@ def main(): anonymized_content = anonymizer.anonymize(orig_content, str(file)) logger.info("[" + str(file) + "] Completed Anonymization") + os.makedirs('Output', exist_ok=True) output_filename = join("Output/" + str(os.path.basename(file))) output_file = open(output_filename, 'w') logger.info("[" + str(file) + "] Saving anonymized content in " + str(output_filename)) diff --git a/app/assets/images/SSID_Logo.svg b/app/assets/images/SSID_Logo.svg new file mode 100644 index 00000000..26c233b5 --- /dev/null +++ b/app/assets/images/SSID_Logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/add_circle_outline_blue_24dp.svg b/app/assets/images/add_circle_outline_blue_24dp.svg index 343ede7c..20fe0966 100644 --- a/app/assets/images/add_circle_outline_blue_24dp.svg +++ b/app/assets/images/add_circle_outline_blue_24dp.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/avatar-icon.svg b/app/assets/images/avatar-icon.svg new file mode 100644 index 00000000..b679441d --- /dev/null +++ b/app/assets/images/avatar-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/btn_google_signin_dark_normal_web.png b/app/assets/images/btn_google_signin_dark_normal_web.png new file mode 100644 index 00000000..b1327b4f Binary files /dev/null and b/app/assets/images/btn_google_signin_dark_normal_web.png differ diff --git a/app/assets/images/chart-icon.svg b/app/assets/images/chart-icon.svg new file mode 100644 index 00000000..dc52964f --- /dev/null +++ b/app/assets/images/chart-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/check_icon.svg b/app/assets/images/check_icon.svg new file mode 100644 index 00000000..2a99381f --- /dev/null +++ b/app/assets/images/check_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/done_24dp.svg b/app/assets/images/done_24dp.svg deleted file mode 100644 index db923774..00000000 --- a/app/assets/images/done_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/images/gradient-divider.png b/app/assets/images/gradient-divider.png new file mode 100644 index 00000000..39b18dc3 Binary files /dev/null and b/app/assets/images/gradient-divider.png differ diff --git a/app/assets/images/integration-icon.svg b/app/assets/images/integration-icon.svg new file mode 100644 index 00000000..10e1b9f4 --- /dev/null +++ b/app/assets/images/integration-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/lightning-icon.svg b/app/assets/images/lightning-icon.svg new file mode 100644 index 00000000..02e235b8 --- /dev/null +++ b/app/assets/images/lightning-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/p13.png b/app/assets/images/p13.png index 1a9f81bc..1ffcda1f 100644 Binary files a/app/assets/images/p13.png and b/app/assets/images/p13.png differ diff --git a/app/assets/images/person_add_blue_24dp.svg b/app/assets/images/person_add_blue_24dp.svg index 3b64c806..3a61d764 100644 --- a/app/assets/images/person_add_blue_24dp.svg +++ b/app/assets/images/person_add_blue_24dp.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/plagiarism_cover_image.svg b/app/assets/images/plagiarism_cover_image.svg new file mode 100644 index 00000000..dd8c5e44 --- /dev/null +++ b/app/assets/images/plagiarism_cover_image.svg @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/images/plus-circle.svg b/app/assets/images/plus-circle.svg index 295922bd..94b5eafb 100644 --- a/app/assets/images/plus-circle.svg +++ b/app/assets/images/plus-circle.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/app/assets/images/ssid_cover_screenshot.png b/app/assets/images/ssid_cover_screenshot.png new file mode 100644 index 00000000..13447e0e Binary files /dev/null and b/app/assets/images/ssid_cover_screenshot.png differ diff --git a/app/assets/javascripts/site.js.coffee b/app/assets/javascripts/site.js.coffee index ff635046..4ba1bf65 100644 --- a/app/assets/javascripts/site.js.coffee +++ b/app/assets/javascripts/site.js.coffee @@ -52,10 +52,16 @@ Site.setClipHeight = (page) -> return $(document).ready -> - if $("ul#menu").length > 0 - $(".site_header").addClass("site_header_background"); Site.setClipHeight($(this)); - if (window.location.pathname && !window.location.pathname.includes('cover')) - $(".site-login").addClass("site-login-hidden") + if $(".site-introduction").length > 0 + $("body").css('background-image', "radial-gradient( + at top right, + #d4e5ea, + white 75% + )"); + $("body").css('background-repeat', "no-repeat"); + + # if (window.location.pathname && !window.location.pathname.includes('cover')) + # $(".site-login").addClass("site-login-hidden") return diff --git a/app/assets/javascripts/submission_similarities.js b/app/assets/javascripts/submission_similarities.js index dda50ad3..b94f4882 100644 --- a/app/assets/javascripts/submission_similarities.js +++ b/app/assets/javascripts/submission_similarities.js @@ -17,6 +17,29 @@ along with SSID. If not, see . (function() { window.SubmissionSimilarity || (window.SubmissionSimilarity = {}); + + SubmissionSimilarity.changeViewType = function(el) { + viewType = $(el).val() + if (viewType == "Interactive") { + $("div.side-by-side").hide(); + $("div.interactive").show(); + interactiveCSS = { + "overflow-y": "auto", + "height": "700px" + } + $("div.submission1").css(interactiveCSS); + $("div.submission2").css(interactiveCSS); + } else { + $("div.interactive").hide(); + $("div.side-by-side").show(); + sideBySideCSS = { + "overflow-y": "", + "height": "auto" + } + $("div.submission1").css(sideBySideCSS); + $("div.submission2").css(sideBySideCSS); + } + }; SubmissionSimilarity.slideToLine = function(checkBox) { var s1y, s2y, startLine1, startLine2, values; diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index d8769c0e..072e1443 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -15,15 +15,92 @@ */ // Custom bootstrap variables must be set or imported *before* bootstrap. @import "bootstrap_custom"; -// @import "announcements"; -// @import "assignments"; -// @import "courses.css"; -// @import "scaffolds.css"; -// @import "sessions.css"; -// @import "site.css"; -// @import "submission_cluster_groups.css"; - -// @import "submission_clusters"; -// @import "submission_logs.css"; -// @import "submission_similarities.css"; -// @import "users.css"; \ No newline at end of file + // Other component specific styles (uncomment as necessary) + // @import "announcements"; + // @import "assignments"; + // @import "courses"; + // @import "scaffolds"; + // @import "sessions"; + // @import "site"; + // @import "submission_cluster_groups"; + // @import "submission_clusters"; + // @import "submission_logs"; + // @import "submission_similarities"; + // @import "users"; + + // --------------------------------------------------------------------------- + // 2. Application-Wide Variables + // --------------------------------------------------------------------------- + + // Defining common color variables for reuse throughout the application + $color-error: #b94a48; + $color-success: #468847; + $background-error: #f2dede; + $background-success: #dff0d8; + $primary-color: #3b76f6; + $secondary-color: #64748B; + $signup-hover-bg: #2563eb; + $signup-hover-color: white; + + // --------------------------------------------------------------------------- + // 3. Component-Specific Styles + // --------------------------------------------------------------------------- + + // Alerts - Different variations for feedback messages + .alert-error, .alert-alert { + background-color: $background-error; + border-color: lighten($background-error, 5%); + color: $color-error; + text-align: left; + } + + .alert-success, .alert-notice { + background-color: $background-success; + border-color: lighten($background-success, 5%); + color: $color-success; + text-align: left; + } + + // Buttons - Primary actions and form submissions + .btn-signup { + @extend .btn-primary; + background-color: $primary-color; + height: 3rem; + color: white; + + &:hover { + background-color: $signup-hover-bg; + color: $signup-hover-color; + } + + &:active { + border-style: outset; + } + } + + .btn-login { + background-color: transparent; + height: 3rem; + color: $secondary-color; + padding: 1rem; + } + + // --------------------------------------------------------------------------- + // 4. Helper Classes and Utilities + // --------------------------------------------------------------------------- + + // [Add helper classes and utility styles here, such as text utilities, spacing, or visibility helpers] + + // --------------------------------------------------------------------------- + // 5. Responsive Adjustments + // --------------------------------------------------------------------------- + + // Media queries for responsive breakpoints and adaptive styling + @media (max-width: 768px) { + + + // [Add other responsive adjustments here] + } + + // Additional notes or comments can be added below as required for ongoing development documentation. + \ No newline at end of file diff --git a/app/assets/stylesheets/password_resets.scss b/app/assets/stylesheets/password_resets.scss deleted file mode 100644 index ddf6c3e3..00000000 --- a/app/assets/stylesheets/password_resets.scss +++ /dev/null @@ -1,74 +0,0 @@ -// Place all the styles related to the PasswordResets controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: https://sass-lang.com/ - -// This file is part of SSID. -// -// SSID is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// SSID is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with SSID. If not, see . -@import "bootstrap_custom"; - -body.password_resets { - .site { - @extend .text-center; - width: 60%; - margin: auto; - } - - .forget-password-helper-text { - @extend .mt-3; - @extend .mb-3; - } - - .site_copyright { - @extend .mt-5; - @extend .mb-3; - @extend .text-muted; - } - - .site-password-reset-link { - @extend .w-100; - @extend .btn; - @extend .btn-primary; - } - - form#reset-password { - label { - margin-top: $spacer * 0.5; - } - } - - .send-password-reset-link { - h5 { - margin-bottom: $spacer; - } - - div { - p { - margin-bottom: $spacer * 0.5; - } - } - } - - .reset-password { - h5 { - margin-bottom: $spacer; - } - } - - .error-explanation { - color: var(--bs-danger); - display: none; - } -} - diff --git a/app/assets/stylesheets/scaffolds.css.scss b/app/assets/stylesheets/scaffolds.css.scss index 2f804812..551cbf06 100644 --- a/app/assets/stylesheets/scaffolds.css.scss +++ b/app/assets/stylesheets/scaffolds.css.scss @@ -12,6 +12,7 @@ // // You should have received a copy of the GNU Lesser General Public License // along with SSID. If not, see . +@import url("http://fonts.googleapis.com/css?family=Inter:400,300,500,600,700"); @import "bootstrap/functions"; @import "bootstrap/variables"; @import "bootstrap/mixins"; @@ -22,7 +23,7 @@ body { background-color: #fff; color: var(--bs-body-color); - font-family: var(--bs-font-sans-serif); + font-family: 'Inter', sans-serif; font-size: var(--bs-body-font-size); font-weight: var(--bs-body-font-weight); line-height: var(--bs-body-line-height); @@ -35,7 +36,7 @@ body { } p, ol, ul, li, td { - font-family: var(--bs-font-sans-serif); + font-family: 'Inter', sans-serif; font-size: 0.9375rem; } diff --git a/app/assets/stylesheets/sessions.css.scss b/app/assets/stylesheets/sessions.css.scss index 8a19faaa..ab38a8fa 100644 --- a/app/assets/stylesheets/sessions.css.scss +++ b/app/assets/stylesheets/sessions.css.scss @@ -13,34 +13,16 @@ // You should have received a copy of the GNU Lesser General Public License // along with SSID. If not, see . +@import "bootstrap/functions"; +@import "bootstrap/variables"; +@import "bootstrap/mixins"; +@import "bootstrap/utilities"; + body.sessions_new { position: relative; - h2 { - font-size: 24px; - margin-top: 50px; - display: block; - text-align: center; - } - - fieldset { - display: block; - width: 210px; - position: relative; - left: 50%; - margin-left: -115px; - margin-top: 10px; - - label { - display: inline-block; - width: 70px; - } - - span { - margin-top: 10px; - display: block; - text-align: center; - } + div.user-login { + margin-top: $spacer * 2; } div.alert { diff --git a/app/assets/stylesheets/site.css.scss b/app/assets/stylesheets/site.css.scss index 6e7ff14c..fed3e154 100644 --- a/app/assets/stylesheets/site.css.scss +++ b/app/assets/stylesheets/site.css.scss @@ -14,10 +14,20 @@ // along with SSID. If not, see . @import "bootstrap_custom"; +// Define color variables +$accent-color: #2563eb; +$header-color: #27364B; +$text-color: #64748B; +$btn-default-color: #3b76f6; +$btn-hover-color: #2563eb; + +html { + height: 100%; +} + body { margin: 0; padding: 0; - min-width: 800px; div.alert { color: var(--bs-red); @@ -25,7 +35,7 @@ body { } h2, h3, h4, h5, h6 { - color: #003d7c; + color: $header-color; } .site { @@ -49,86 +59,153 @@ h2, h3, h4, h5, h6 { .site-login-hidden { display: none; } -} -.site_header_background { - background-color: var(--bs-gray-200); } -.site_logo { - padding: 1.25rem 0.5rem; +.menu-btn { + display: none; } -.site-introduction { - width: 100%; - display: flex; - flex-direction: row; - margin-bottom: $spacer * 2; +@media (max-width: 768px) { + .site-login { + display: none; + margin: 0; + padding: 0; + list-style: none; + overflow: hidden; + background-color: #fff; + } - .site-motto { - width: 40%; - font-size: 1.25rem; - font-weight: 500; - font-style: italic; - padding: 0.5rem 0 0 0.5rem; + .menu-btn { + display: none; } - div.site-clip { - width: 60%; + .menu-btn:checked ~ .site-login { + display: block; + position: absolute; + left: 0; + top: 0; + border-radius: 10px; + border-width: 1px; + border-color: gray; + border-style: solid; + } - iframe { - width: 100%; - height: 100%; - } + .menu-icon { + display: inline-block; + cursor: pointer; + position: absolute; + top: 50%; } -} -.site-features-wrapper { - width: 100%; - display: flex; - flex-direction: row; + .menu-icon-container { + position: relative; + display: inline-block; + width: 32px; + } - .site-statistics { - width: 50%; + .navicon { + background: #333; + display: block; + height: 2px; + position: relative; + transition: background .2s ease-out; + width: 18px; } - .site-features { - width: 50%; + .navicon:before, + .navicon:after { + background: #333; + content: ''; + display: block; + height: 100%; + position: absolute; + transition: all .2s ease-out; + width: 100%; + } + + .navicon:before { + top: 5px; + } + + .navicon:after { + top: -5px; + } + + .menu-btn:checked ~ .menu-icon .navicon { + background: transparent; + } + + .menu-btn:checked ~ .menu-icon .navicon:before { + transform: rotate(-45deg); + } + + .menu-btn:checked ~ .menu-icon .navicon:after { + transform: rotate(45deg); + } + + .menu-btn:checked ~ .menu-icon:not(.steps) .navicon:before, + .menu-btn:checked ~ .menu-icon:not(.steps) .navicon:after { + top: 0; } } -.site-footer { - width: 100%; - display: flex; - flex-direction: row; +.site-faq { + padding: 4rem 2rem; - span { - width: 50%; - margin: 0 0 1rem; - @extend .text-muted; + .faq-heading { + text-align: center; + width: 100%; + padding-top: $spacer * 4; + padding-bottom: $spacer * 4; } + .accordion-button { + font: 600 18px/20px Inter, sans-serif; + min-width: 80vw; + background: white !important; + color: black !important; + } +} - ul { - width: 50%; - height: fit-content; - list-style: none; - text-align: right; - padding: 0 1rem 0 2rem; +.accordion-item { + border: 0; +} - li { - display: inline-block; - a { - text-decoration: none; - } - } +.accordion-body { + font: 400 16px/18px Inter, sans-serif; + width: 80vw; + line-height: 150%; +} + +.faq-container { + align-items: center; + align-self: stretch; + background-color: var(--White, #fff); + display: flex; + flex-direction: column; + padding: 3rem 2rem 8rem; +} + +@media (max-width: 768px) { + .site-faq { + padding: 4rem 2rem; } } +.faq-content { + display: flex; + width: 100%; + max-width: 1216px; + flex-direction: column; + align-items: center; + margin: 96px 0; +} + .forget-password-helper-text, .login_helper_text { @extend .mt-3; @extend .mb-3; @extend .fw-normal; - color: #003d7c; + color: $accent-color; } .site_copyright { @@ -159,11 +236,10 @@ table { #site-banner-login { margin: 0; height: 75%; - width: 60%; padding: 1.25rem 0.5rem; font-size: 2rem; - font-weight: 500; - color: #003d7c; + font-weight: 700; + color: $accent-color; } #site_banner { @@ -173,7 +249,7 @@ table { padding: 1rem 0.5rem; font-size: 2rem; font-weight: 400; - color: #003d7c; + color: $accent-color; } #menu { @@ -195,13 +271,13 @@ table { color: var(--bs-gray-600); } &:hover { - color: #003d7c; + color: $accent-color; border-bottom: 3px solid #ef7c00; } } &.active_menu_item a { - color: #003d7c; + color: $accent-color; border-bottom: 3px solid #ef7c00; } } @@ -231,3 +307,699 @@ div.notice { margin-bottom: $spacer; color: rgb(0, 160, 0); } + +// HERO SECTION +.hero-section { + width: 100vw; + left: calc(-50vw + 50%); + position: relative; + align-items: center; + align-self: stretch; + display: flex; + flex-direction: column; + padding: 0 11px; + margin-bottom: 3rem; +} + +.hero-content { + margin-top: -2rem; + display: flex; + width: 100%; + max-width: 1024px; + flex-direction: column; +} + +@media (max-width: 768px) { + .hero-content { + max-width: 100%; + } +} + +.hero-header { + display: flex; + flex-direction: column; +} + +.hero-title { + color: #27364b; + text-align: center; + letter-spacing: -0.96px; + font: 600 48px/51.5px Inter, sans-serif; + + em { + font-style: normal; + background: -webkit-linear-gradient(rgb(116, 61, 245), $accent-color); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } +} + +@media (max-width: 768px) { + .hero-title { + max-width: 100%; + font-size: 40px; + line-height: 48px; + } +} + +.hero-subtitle { + color: var(--gray-500, #667085); + text-align: center; + align-self: center; + margin-top: 24px; + max-width: 649px; + font: 400 20px/30px Inter, sans-serif; +} + +@media (max-width: 768px) { + .hero-subtitle { + max-width: 100%; + } +} + +.hero-buttons { + align-self: center; + display: flex; + margin-top: 48px; + max-width: 100%; + gap: 12px; +} + +@media (max-width: 768px) { + .hero-buttons { + margin-top: 40px; + flex-direction: column; + } +} + +.site_logo { + padding: 1.25rem 0.5rem; +} + +.button-filled { + color: var(--White, #fff); + white-space: nowrap; + justify-content: center; + align-items: center; + border-style: none; + border-radius: 8px; + box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); + background-color: $btn-default-color; + padding: 0.5rem 1.5rem; + font: 400 16px/28px Inter, sans-serif; +} + +.button-filled:hover { + background-color: $btn-hover-color; +} + +@media (max-width: 768px) { + .button-filled { + white-space: initial; + } +} + +.button-outline { + color: var(--gray-700, #344054); + white-space: nowrap; + justify-content: center; + align-items: center; + border-radius: 8px; + border: 1px solid var(--gray-300, #d0d5dd); + box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); + background-color: var(--White, #fff); + padding: 0.5rem 1.5rem; + font: 400 16px/28px Inter, sans-serif; +} + +.button-outline:hover { + background-color: #efefef; +} + +@media (max-width: 768px) { + .button-outline { + white-space: initial; + } +} + +.hero-images { + margin-top: -7rem; + display: flex; + flex-direction: column; + overflow: hidden; + align-self: stretch; + position: relative; + align-items: center; + padding: 12px 25vw 0; + z-index: -1; +} + +@media (max-width: 768px) { + .hero-images { + margin-top: 0rem; + padding: 0; + z-index: -1; + } +} + +// FEATURES SECTION + +.features-container { + width: 100vw; + left: calc(-50vw + 50%); + position: relative; + align-items: center; + align-self: stretch; + background-color: var(--White, #fff); + display: flex; + flex-direction: column; + padding: 0 20px; + margin-bottom: 5rem; +} + +.features-content { + display: flex; + width: 100%; + max-width: 1216px; + flex-direction: column; + align-items: center; +} + +@media (max-width: 768px) { + .features-content { + max-width: 100%; + margin: 40px 0; + } +} + +.section-header { + display: flex; + width: 768px; + max-width: 100%; + flex-direction: column; + margin-bottom: 2rem; +} + +.header-content { + display: flex; + flex-direction: column; +} + +.title { + color: #0473e6; + text-align: center; + font: 600 16px/24px Inter, sans-serif; +} + +.subtitle { + color: var(--gray-900, #101828); + text-align: center; + letter-spacing: -0.72px; + margin-top: 12px; + font: 600 36px/44px Inter, sans-serif; +} + +.description { + color: var(--gray-500, #667085); + text-align: center; + margin-top: 20px; + font: 400 20px/30px Inter, sans-serif; +} + +.features-list { + align-self: stretch; + margin-top: 64px; +} + +@media (max-width: 768px) { + .features-list { + max-width: 100%; + margin-top: 40px; + } +} + +.feature-columns { + gap: 20px; + display: flex; +} + +@media (max-width: 768px) { + .feature-columns { + flex-direction: column; + align-items: stretch; + gap: 0px; + } +} + +.feature-column { + display: flex; + flex-direction: column; + line-height: normal; + width: 33%; +} + +@media (max-width: 768px) { + .feature-column { + width: 100%; + } +} + +.feature-item { + align-items: center; + display: flex; + flex-grow: 1; + flex-direction: column; +} + +@media (max-width: 768px) { + .feature-item { + margin-top: 32px; + } +} + +.feature-image { + aspect-ratio: 1; + object-fit: contain; + object-position: center; + width: 48px; + justify-content: center; + align-items: center; + overflow: hidden; + max-width: 100%; +} + +.feature-details { + align-self: stretch; + display: flex; + margin-top: 20px; + flex-direction: column; +} + +.feature-title { + color: var(--gray-900, #101828); + text-align: center; + font: 600 20px/30px Inter, sans-serif; +} + +.feature-description { + color: var(--gray-500, #667085); + text-align: center; + margin-top: 8px; + font: 400 16px/24px Inter, sans-serif; +} + +// TESTIMONIALS + +.testimonials-carousel { + width: 100vw; + left: calc(-50vw + 50%); + align-items: center; + align-self: stretch; + background-color: var(--gray-50, #f9fafb); + display: flex; + flex-direction: column; + padding: 40px 80px; + position: relative; +} + +@media (max-width: 768px) { + .testimonials-carousel { + padding: 40px 20px; + } +} + +.testimonial-item { + align-items: center; + align-self: stretch; + display: flex; + flex-direction: column; + text-align: center; + max-width: 1216px; + width: 100%; + margin: 0 auto; +} + +.quote-mark { + color: var(--gray-900, #101828); + font: 400 96px/60px Georgia, -apple-system, serif; + margin-top: 3rem; +} + +@media (max-width: 768px) { + .quote-mark { + font-size: 40px; + line-height: 28px; + margin-top: 40px; + } +} + +.testimonial-text { + color: #667085; + font: 400 16px/18px Inter, sans-serif; + line-height: 200%; + max-width: 742px; +} + +.testimonial-image { + aspect-ratio: 1; + object-fit: contain; + width: 64px; + margin-top: 32px; +} + +.author-name { + color: var(--gray-900, #101828); + font: 600 18px/20px Inter, sans-serif; + margin-top: 16px; +} + +.author-details { + color: var(--gray-500, #667085); + font: 400 16px/18px Inter, sans-serif; + margin: 4px 0 2rem; +} + +.carousel-button { + position: absolute; + top: 50%; + transform: translateY(-50%); + background-color: #fff; + border: none; + cursor: pointer; + padding: 10px; + font-size: 24px; +} + +.carousel-button.left { + left: 10px; +} + +.carousel-button.right { + right: 10px; +} + +@media (max-width: 768px) { + .author-details, .author-name, .testimonial-text { + max-width: 100%; + } +} + +// ACHIEVEMENTS + +.achievements-section { + align-items: center; + background-color: var(--White, #fff); + display: flex; + flex-direction: column; + padding: 0 20px; +} + +.achievements-content { + display: flex; + flex-direction: column; + margin: 96px 0; + max-width: 1216px; + width: 100%; +} + +.section-title { + color: #0473e6; + font: 600 16px/24px Inter, sans-serif; +} + +.section-subtitle { + align-self: start; + color: var(--gray-900, #101828); + font: 600 36px/44px Inter, sans-serif; + letter-spacing: -0.72px; + margin-top: 12px; + max-width: 100%; + width: 768px; +} + +.section-description { + align-self: start; + color: var(--gray-500, #667085); + font: 400 20px/30px Inter, sans-serif; + margin-top: 20px; + max-width: 100%; + width: 768px; +} + +.statistics { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-top: 64px; +} + +.statistic-item { + align-self: stretch; + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.statistic-number { + color: #102845; + font: 600 60px/72px Inter, sans-serif; + text-align: center; +} + +.statistic-label { + color: var(--gray-900, #101828); + font: 500 18px/28px Inter, sans-serif; + margin-top: 12px; + text-align: center; +} + +.statistic-text { + color: var(--gray-500, #667085); + font: 400 16px/24px Inter, sans-serif; + margin-top: 8px; + text-align: center; +} + +.achievements-image { + display: flex; + flex-direction: column; + line-height: normal; + margin-left: 20px; + width: 50%; +} + +.achievement-img { + aspect-ratio: 1; + object-fit: contain; + object-position: center; + overflow: hidden; + width: 100%; +} + +@media (max-width: 991px) { + .achievements-content { + margin: 40px 0; + max-width: 100%; + } + + .statistics { + align-items: stretch; + flex-direction: column; + gap: 0; + margin-top: 40px; + } + + .statistic-item { + margin-top: 32px; + } + + .statistic-number { + font-size: 40px; + line-height: 53px; + } + + .achievements-image { + margin-left: 0; + margin-top: 40px; + width: 100%; + } + + .achievement-img { + max-width: 100%; + } +} + +// FOOTER + +.footer-container { + position: relative; + left: calc(-50vw + 50%); + width: 100vw; + align-items: center; + align-self: stretch; + background-color: var(--gray-50, #f9fafb); + display: flex; + flex-direction: column; + padding: 0 80px; + + .footer { + .copyright { + font: 400 16px/18px Inter, sans-serif; + position: absolute; + bottom: 1rem; + left: 3.5rem; + color: $text-color !important; + } + + ul { + list-style: none; + position: absolute; + bottom: 0; + right: 3.5rem; + li { + display: inline-block; + a { + text-decoration: none; + } + } + } + + + @media (max-width: 768px) { + .copyright { + position: relative; + bottom: auto; + left: auto; + width: 100%; + } + + ul { + padding: 0; + list-style: none; + position: relative; + right: auto; + bottom: auto; + width: 100%; + li { + display: inline-block; + a { + text-decoration: none; + } + } + } + } + + a { + color: $text-color !important; + text-decoration: inherit; + margin:0; + } + + } + @media (max-width: 768px) { + .footer { + display: flex; + flex-direction: column; + text-align: center; + } + } +} + +@media (max-width: 768px) { + .footer-container { + padding: 0 20px; + } +} + +.footer-title { + color: var(--gray-900, #101828); + text-align: center; + letter-spacing: -0.72px; + margin-top: 96px; + max-width: 768px; + font: 600 36px/44px Inter, sans-serif; +} + +@media (max-width: 768px) { + .footer-title { + max-width: 100%; + margin-top: 40px; + } +} + +.footer-subtitle { + color: var(--gray-500, #667085); + text-align: center; + margin-top: 20px; + max-width: 768px; + font: 400 20px/30px Inter, sans-serif; +} + +@media (max-width: 768px) { + .footer-subtitle { + max-width: 100%; + } +} + +.footer-buttons { + display: flex; + max-width: 100%; + gap: 12px; + margin: 40px 0 96px; +} + +@media (max-width: 768px) { + .footer-buttons { + margin-bottom: 40px; + } +} + +.footer-button-learn { + color: var(--gray-700, #344054); + white-space: nowrap; + justify-content: center; + border-radius: 8px; + border: 1px solid var(--gray-300, #d0d5dd); + box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); + background-color: var(--White, #fff); + flex-grow: 1; + padding: 12px 20px; + font: 600 16px/24px Inter, sans-serif; +} + +@media (max-width: 768px) { + .footer-button-learn { + white-space: initial; + } +} + +.footer-button-start { + color: var(--White, #fff); + white-space: nowrap; + justify-content: center; + border-radius: 8px; + border: 1px solid #0473e6; + box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); + background-color: #0473e6; + flex-grow: 1; + padding: 12px 20px; + font: 600 16px/24px Inter, sans-serif; +} + +@media (max-width: 768px) { + .footer-button-start { + white-space: initial; + } +} + +.footer-divider { + width: 80%; + color: $header-color; + opacity: 0.05; +} + +// GRADIENT DIVIDER + +.gradient-divider { + height: 150px; +} diff --git a/app/assets/stylesheets/submission_clusters.css.scss.erb b/app/assets/stylesheets/submission_clusters.css.scss.erb index 544255e6..d2204646 100644 --- a/app/assets/stylesheets/submission_clusters.css.scss.erb +++ b/app/assets/stylesheets/submission_clusters.css.scss.erb @@ -48,7 +48,7 @@ body.submission_clusters_index { h6 { text-align: center; margin: 3px; - color: #003d7c; + color: #2563eb; } div { diff --git a/app/assets/stylesheets/submission_similarities.css.scss b/app/assets/stylesheets/submission_similarities.css.scss index 0bdd4ee0..04d708f8 100644 --- a/app/assets/stylesheets/submission_similarities.css.scss +++ b/app/assets/stylesheets/submission_similarities.css.scss @@ -44,11 +44,6 @@ body.submission_similarities_index { } body.submission_similarities_show { - .submission_similarity_header { - margin-top: $spacer * 0.75; - margin-bottom: $spacer * 0.5; - } - .table-container { height: 12rem; overflow-y: scroll; @@ -65,23 +60,40 @@ body.submission_similarities_show { margin-right: $spacer; } } - + div.submission1 { + overflow-y: auto; + height: 700px; + } + + div.submission2 { + overflow-y: auto; + height: 700px; + } +} + +body { + .submission_similarity_header { + margin-top: $spacer * 0.75; + margin-bottom: $spacer * 0.5; + } + + h5 { + text-align: center; + margin-bottom: $spacer * 0.75; + } + div.submissions { - margin-top: $spacer * 3; + margin-top: 0px; + margin-bottom: 0px; overflow-y: auto; width: 100%; - h5 { - text-align: center; - margin-bottom: $spacer * 0.75; - } - div.submissions-legend { margin-bottom: $spacer * 0.5; ul { list-style: none; - margin: $spacer*0.5 0; + margin: $spacer * 0.5; padding: 0px; li { @@ -111,17 +123,11 @@ body.submission_similarities_show { div.submission1 { float: left; width: 50%; - overflow-y: auto; - height: 700px; - margin-bottom: $spacer * 3; } div.submission2 { float: left; width: 50%; - overflow-y: auto; - height: 700px; - margin-bottom: $spacer * 3; - } + } } -} +} \ No newline at end of file diff --git a/app/assets/stylesheets/user_course_memberships.css.scss b/app/assets/stylesheets/user_course_memberships.css.scss new file mode 100644 index 00000000..ba9ab248 --- /dev/null +++ b/app/assets/stylesheets/user_course_memberships.css.scss @@ -0,0 +1,58 @@ +// This file is part of SSID. +// +// SSID is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// SSID is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with SSID. If not, see . +@import "bootstrap/functions"; +@import "bootstrap/variables"; +@import "bootstrap/mixins"; +@import "bootstrap/utilities"; + +@import "bootstrap/tables"; + +body.user_course_memberships { + h4 { + margin-top: $spacer * 1.25; + } + + .course_user_header { + display: flex; + flex-direction: row; + justify-content: flex-start; + margin-bottom: $spacer * 0.5; + } + + .user_header { + margin-top: $spacer * 0.75; + } + + h5 { + margin-right: $spacer * 1.5; + } + + table { + @extend .table; + @extend .table-bordered; + @extend .table-hover; + } + + .course_users_section { + margin-bottom: $spacer * 3; + } + + form { + label { + margin-top: $spacer * 0.5; + display: inline-block; + } + } +} diff --git a/app/assets/stylesheets/users.css.scss b/app/assets/stylesheets/users.css.scss index 965004bf..36803c44 100644 --- a/app/assets/stylesheets/users.css.scss +++ b/app/assets/stylesheets/users.css.scss @@ -81,3 +81,23 @@ body.users_index { margin-bottom: $spacer * 5; } } + +// Devise +body { + .users-shared-links { + a { + text-decoration: none; + } + + .omniauth-links { + margin: $spacer * 0.5 0; + } + + } +} + +body.registrations_new { + div.user-signup { + margin-top: $spacer * 2; + } +} diff --git a/app/controllers/account_activations_controller.rb b/app/controllers/account_activations_controller.rb deleted file mode 100644 index 9567601c..00000000 --- a/app/controllers/account_activations_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -class AccountActivationsController < ApplicationController - skip_before_action :authorize - def edit - user = User.find_by(email: params[:email]) - if user && !user.activated? && user.authenticated?(:activation, params[:id]) - user.update_attribute(:activated, true) - user.update_attribute(:activated_at, Time.zone.now) - flash[:notice] = "Account activated! And it will be processed shortly by our admins." - redirect_to root_url - else - flash[:alert] = "Invalid activation link" - redirect_to root_url - end - end -end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 9e6a20d0..a04caba2 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -16,140 +16,18 @@ =end class Admin::UsersController < ApplicationController - - GUEST_USER_ROLE_ID = '3' - - before_action { |controller| - - # obtain course (if any) to determine whether the user has any course membership - begin - @course = Course.find(params[:course_id]) - rescue ActiveRecord::RecordNotFound - @course = Course.find(params[:user]["course_id"]) rescue nil - end - - controller.send :authenticate_actions_for_admin_or_teaching_staff, - course: @course, - only: [ :index, :new, :create, :edit, :update, :destroy ] # all methods - } + + before_action :is_admin, only: [:index, :approve, :upgrade_to_admin_account, :destroy] # GET /admin/users def index @admins = User.where(is_admin: true) - - @signups = User.where(is_admin_approved: false) + @students = User.joins(:courses, :memberships).where(users: { is_admin_approved: false}).where(user_course_memberships: { role: UserCourseMembership::ROLE_STUDENT }) + @signups = User.where(is_admin_approved: false) - @students # users don't belong to any course @loners = User.all - User.joins(:memberships) - @admins - @signups - - - end - - # GET /admin/users/new - # GET /admin/users/new?course_id=1 - def new - @the_user = User.new - @course = nil - if params[:course_id] - @course = Course.find(params[:course_id]) - end - end - - # POST /admin/users/ - def create - @the_user = User.new - @course = nil - - # Check if we have a course_id - if params[:user]["course_id"] - @course = Course.find(params[:user]["course_id"]) - @the_user.errors.add :base, "Course could not be found." unless @course - end - - # Construct basic attributes for course user or for admin user - if !@course.nil? - # Check if user exists - @existing_user = User.where(name: params[:user]["name"]).first - if @existing_user - @the_user = @existing_user - else - @the_user.full_name = params[:user]["full_name"] - @the_user.name = params[:user]["name"] - @the_user.email = params[:user]["email"] - end - else - @the_user.is_admin = true - @the_user.full_name = params[:user]["full_name"] - @the_user.name = params[:user]["name"] - @the_user.email = params[:user]["email"] - end - - # Validate password, email unless its an existing user - unless @existing_user - UsersHelper.validate_password(@the_user, params[:user]["password"], params[:user]["confirm_password"]) - UsersHelper.validate_email(@the_user, params[:user]["email"]) - - # Set password - @the_user.password_digest = BCrypt::Password.create(params[:user]["password"]) if @the_user.errors.empty? - end - - # Before we can add membership, we need to save the user first, so we use a transaction - if @the_user.errors.empty? - @the_user.transaction do - # Add membership if @course - if @the_user.save and @course - membership = UserCourseMembership.new { |m| - m.role = params[:user]["course_role"] - m.user = @the_user - m.course = @course - } - raise ActiveRecord::Rollback unless membership.save - - # if user is guest, create a entry under guest database as well - if params[:user]["course_role"] == GUEST_USER_ROLE_ID - guest = GuestUsersDetail.new { |g| - g.user_id = @the_user.id - g.course_id = @course.id - g.hash_string = params[:user]["name"] - g.assignment_id = 0 - } - raise ActiveRecord::Rollback unless guest.save - end - end - end - end - - @user = User.find_by_id(session[:user_id]) - - # Check for errors and render view - if @the_user.errors.empty? and @the_user.save - unless @existing_user - UserMailer.account_activation(@the_user).deliver_now - end - - if @existing_user or not @course.nil? - redirect_to course_users_url(@course), notice: "User was successfully added - to #{@course.code}, and account needs to be activated before use." - else - redirect_to admin_users_url, notice: 'User was successfully created, and account needs to be activated before use.' - end - else - render action: "new" - end - - end - - # GET /admin/users/1/edit - # GET /admin/users/1/edit?course_id=1 - def edit - @the_user = User.find(params[:id]) - @course = nil - @membership = nil - if params[:course_id] - @course = Course.find(params[:course_id]) - @membership = @course.membership_for_user(@the_user) - end - end + end def approve @the_user = User.find(params[:user_id]) @@ -159,71 +37,15 @@ def approve redirect_to admin_users_url, notice: 'User was successfully approved.' end - - - # PUT /admin/users/1 - def update - @the_user = User.find(params[:id]) - @membership = nil - @course = nil - - # Check if we have a course_id - if params[:user]["course_id"] - @course = Course.find(params[:user]["course_id"]) - @the_user.errors.add :base, "Course could not be found." unless @course - end - - # Update basic attributes for course user or for admin user - if @course - @the_user.full_name = params[:user]["full_name"] - @the_user.name = params[:user]["name"] - @the_user.email = params[:user]["email"] - - # Update role unless student - @membership = @course.membership_for_user(@the_user) - unless @membership.role == UserCourseMembership::ROLE_STUDENT - @membership.role = params[:user]["course_role"] - end - else - @the_user.full_name = params[:user]["full_name"] - @the_user.name = params[:user]["name"] - @the_user.email = params[:user]["email"] - end - - # Validate password unless blank - unless params[:user]["new_password"].blank? - UsersHelper.validate_password(@the_user, params[:user]["new_password"], params[:user]["confirm_new_password"]) - - # Set password - @the_user.password_digest = BCrypt::Password.create(params[:user]["new_password"]) if @the_user.errors.empty? - end - - # Validate email - UsersHelper.validate_email(@the_user, params[:user]["email"]) - - # Save in transaction - if @the_user.errors.empty? - @the_user.transaction do - raise ActiveRecord::Rollback unless @the_user.save - if @membership and not @membership.save - @the_user.errors.add :base, @membership.errors.to_a.join(", ") - raise ActiveRecord::Rollback - end - end - end - - # Check for errors and render view - if @the_user.errors.empty? - if @course - redirect_to course_users_url(@course), notice: "#{@course.code} User was successfully updated." - else - redirect_to admin_users_url, notice: "Admin User was successfully updated." - end - else - render action: "edit" - end + def upgrade_to_admin_account + @the_user = User.find(params[:user_id]) + @the_user.update_attribute(:is_admin, true) + @the_user.save + logger.info("#{Time.now} Account id #{@the_user.id} was upgraded to admin by #{current_user.id}") + redirect_to admin_users_url, notice: 'User was successfully upgraded to admin account.' end - + + # TODO: refactor later # DELETE /admin/users/1 # DELETE /admin/users/1?course_id=1 def destroy @@ -263,4 +85,12 @@ def destroy end end end + + protected + + def is_admin + unless current_user.is_admin + redirect_to root_url, status: 401 + end + end end diff --git a/app/controllers/announcements_controller.rb b/app/controllers/announcements_controller.rb index 5482cd65..5207bffd 100644 --- a/app/controllers/announcements_controller.rb +++ b/app/controllers/announcements_controller.rb @@ -30,14 +30,15 @@ class AnnouncementsController < ApplicationController # GET /announcements def index - @announcements = @user.courses.collect { |c| c.announcements }.flatten + @announcements = current_user.courses.collect { |c| c.announcements }.flatten + end # GET /announcements/new def new @announcement = Announcement.new - @announceable_courses = @user.courses.select { |course| - course.membership_for_user(@user).role == UserCourseMembership::ROLE_TEACHING_STAFF + @announceable_courses = current_user.courses.select { |course| + course.membership_for_user(current_user).role == UserCourseMembership::ROLE_TEACHING_STAFF } end @@ -56,8 +57,8 @@ def create end # Check permissions - @announceable_courses = @user.courses.select { |course| - course.membership_for_user(@user).role == UserCourseMembership::ROLE_TEACHING_STAFF + @announceable_courses = current_user.courses.select { |course| + course.membership_for_user(current_user).role == UserCourseMembership::ROLE_TEACHING_STAFF } if course and @announceable_courses.include? course @announcement.announceable = course @@ -75,8 +76,8 @@ def create # GET /announcements/1/edit def edit @announcement = Announcement.find(params[:id]) - @announceable_courses = @user.courses.select { |course| - course.membership_for_user(@user).role == UserCourseMembership::ROLE_TEACHING_STAFF + @announceable_courses = current_user.courses.select { |course| + course.membership_for_user(current_user).role == UserCourseMembership::ROLE_TEACHING_STAFF } end @@ -96,8 +97,8 @@ def update end # Check permissions - @announceable_courses = @user.courses.select { |course| - course.membership_for_user(@user).role == UserCourseMembership::ROLE_TEACHING_STAFF + @announceable_courses = current_user.courses.select { |course| + course.membership_for_user(current_user).role == UserCourseMembership::ROLE_TEACHING_STAFF } if course and @announceable_courses.include? course @announcement.announceable = course diff --git a/app/controllers/api/v1/assignments_controller.rb b/app/controllers/api/v1/assignments_controller.rb new file mode 100644 index 00000000..10ddb354 --- /dev/null +++ b/app/controllers/api/v1/assignments_controller.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +# This file is part of SSID. +# +# SSID is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SSID is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with SSID. If not, see . + +require 'zip' +require 'api_keys_handler' + +module Api + module V1 + class AssignmentsController < ApplicationController + skip_before_action :authenticate_user! + + before_action do |_controller| + @course = Course.find(params['course_id']) if params['course_id'] + end + + before_action :init_api_key_handler + + # Define valid zip mime types as constant variables + X_ZIP_COMPRESSED_MIME_TYPE = 'application/x-zip-compressed' + ZIP_COMPRESSED_MIME_TYPE = 'application/zip-compressed' + APPLICATION_ZIP_MIME_TYPE = 'application/zip' + MULTIPART_X_ZIP_MIME_TYPE = 'multipart/x-zip' + OCTET_STREAM_MIME_TYPE = 'application/octet-stream' + REQUIRED_PARAMS = %w[title language studentSubmissions].freeze + ALLOWED_PARAMS = %w[title language useFingerprints minimumMatchLength sizeOfNGram studentSubmissions + mappingFile].freeze + ALLOWED_LANGUAGES = %w[java python3 c cpp javascript r ocaml matlab scala].freeze + + def init_api_key_handler + APIKeysHandler.api_key = ApiKey.find_by(value: request.headers['X-API-KEY']) + APIKeysHandler.course = @course + end + + # GET api/v1/courses/1/assignments/new + def new + @assignment = Assignment.new + end + + # POST api/v1/courses/1/assignments + def create + REQUIRED_PARAMS.each do |p| + if params[p].nil? + render json: { error: "Missing required parameter '#{p}'" }, status: :bad_request + return + end + end + + request.request_parameters.each do |k, _v| + if ALLOWED_PARAMS.include?(k) == false + render json: { error: "Parameter #{k} is invalid or not yet supported." }, status: :bad_request + return + end + end + + @assignment = Assignment.new do |a| + a.title = params['title'] + a.language = params['language'] + a.min_match_length = params['minimumMatchLength'].presence || 2 # defaults to 2 if not specified + a.ngram_size = params['sizeOfNGram'].presence || 5 # defaults to 5 if not specified + a.course_id = @course.id + end + + begin + APIKeysHandler.authenticate_api_key + rescue APIKeysHandler::APIKeyError => e + render json: { error: e.message }, status: e.status + return + end + + REQUIRED_PARAMS.each do |p| + if params[p].nil? + render json: { error: "Missing required parameter '#{p}'" }, status: :bad_request + return + end + end + + unless ALLOWED_LANGUAGES.include?(params['language']) + render json: { error: "Value of language is not valid.' + + 'We currently support #{ALLOWED_LANGUAGES}.' + + 'The parameter value must be in lowercase and match exactly one of the options." }, + status: :bad_request + return + end + + if params['useFingerprints'] && %w[Yes No].exclude?(params['useFingerprints']) + render json: { error: 'Value of useFingerprints is not valid. ' \ + 'The value should be "Yes" or "No". ' \ + 'The parameter value must be in lowercase and match exactly one of the options.' }, + status: :bad_request + return + end + + Rails.logger.debug 'DEBUG 06: Enable fingerprints checkbox?' + Rails.logger.debug { "Checkbox: #{params['useFingerprints']}" } + # Process file if @assignment is valid and file was uploaded + if @assignment.valid? + + # Save assignment to obtain id + return render action: 'new' unless @assignment.save + + is_map_enabled = !params['mappingFile'].nil? + used_fingerprints = params['useFingerprints'] == 'Yes' + + # No student submission file was uploaded + # Student submission file is a valid zip + if valid_zip?(params['studentSubmissions'].content_type, params['studentSubmissions'].path) + # Don't process the file and show error if the mapping was enabled but no mapping file was uploaded + if valid_map_or_no_map?(is_map_enabled, params['mappingFile']) + start_upload(@assignment, params['studentSubmissions'], is_map_enabled, params['mappingFile'], + used_fingerprints) + # Don't process the file and show error if the mapping was enabled but no mapping file was uploaded + else + @assignment.errors.add :mapfile, 'containing mapped student names must be a valid csv file' + render json: { error: "Value of mappingFile is not valid. ' + + 'The mapping file must be a valid csv file." }, + status: :bad_request + end + # Student submission file is not a valid zip file + else + @assignment.errors.add :file, 'containing student submission files must be a valid zip file' + render json: { error: 'Value of studentSubmissions is not valid. ' \ + 'studentSubmissions must be a valid zip file.' }, + status: :bad_request + end + else + render action: 'new' + end + end + + def start_upload(assignment, submission_file, is_map_enabled, map_file, used_fingerprints) + require 'submissions_handler' + + # Process upload file + submissions_path = SubmissionsHandler.process_upload(submission_file, is_map_enabled, map_file, assignment) + if submissions_path + # Launch java program to process submissions + SubmissionsHandler.process_submissions(submissions_path, assignment, is_map_enabled, used_fingerprints) + + render json: { assignmentID: @assignment.id }, status: :ok + else + assignment.errors.add 'Submission zip file', + ': SSID supports both directory-based and file-based submissions. ' \ + 'Please select the submissions you want to evaluate and compress.' + render action: 'show' + end + end + + # Responsible for verifying whether a uploaded file is zip by checking its mime + # type and/or whether can it be extracted by the zip library. + # For files with mime type = application/octet-stream, it needs to be further verified + # by the zip library as it can be a rar file. + # Params: + # +mime_type+:: string that contains the file's mimetype + # +filePath+:: string that contains the file's path which is to be used + # by the zip library when extracting the file + def valid_zip?(mime_type, file_path) + # Valid zip file mime types that does not required to be further verified by the zip library + if [X_ZIP_COMPRESSED_MIME_TYPE, ZIP_COMPRESSED_MIME_TYPE, APPLICATION_ZIP_MIME_TYPE, + MULTIPART_X_ZIP_MIME_TYPE].include?(mime_type) + return true + # Need to be further verified by zip library as it can be a rar file + elsif mime_type == OCTET_STREAM_MIME_TYPE && opened_as_zip?(file_path) + return true + # For other mime types, safe to consider that it is not a zip file + end + false + end + + # Responsible for verifying whether a uploaded file is zip by checking whether can it be extracted by the zip + # library + # Params: + # +filePath+:: string that contains the file's path which is to be used by the zip library + # when extracting the file + def opened_as_zip?(path) + # File is zip if the zip library is able to extract the file + zip = Zip::File.open(path) + true + rescue StandardError => e + Rails.logger.debug e + false + ensure + zip&.close + end + + def valid_map_or_no_map?(is_map_enabled, map_file) + return true unless is_map_enabled + + if map_file.nil? + false + else + map_file.path.split('.').last.to_s.downcase == 'csv' + end + end + end + end +end diff --git a/app/controllers/api/v1/submission_similarities_controller.rb b/app/controllers/api/v1/submission_similarities_controller.rb new file mode 100644 index 00000000..32e0961e --- /dev/null +++ b/app/controllers/api/v1/submission_similarities_controller.rb @@ -0,0 +1,183 @@ +# frozen_string_literal: true + +require 'api_keys_handler' +require 'pdfkit' + +module Api + module V1 + # The `SubmissionSimilaritiesController` is responsible for handling API requests related to + # fetching submission similarities for assignments. It provides an endpoint for clients to + # retrieve submission similarities associated with a given assignment. The API key must be + # provided and validated for access, and the user associated with the API key must have + # authorization for the specified course. + class SubmissionSimilaritiesController < ApplicationController + skip_before_action :authenticate_user! + + before_action do |_controller| + set_api_key_and_assignment + end + + def index + APIKeysHandler.authenticate_api_key + render_submission_similarities + rescue APIKeysHandler::APIKeyError => e + render json: { error: e.message }, status: e.status + end + + def show + APIKeysHandler.authenticate_api_key + render_pair_of_flagged_submissions + rescue APIKeysHandler::APIKeyError => e + render json: { error: e.message }, status: e.status + end + + def view_pdf + APIKeysHandler.authenticate_api_key + generate_pdf + rescue APIKeysHandler::APIKeyError => e + render json: { error: e.message }, status: e.status + end + + private + + def set_api_key_and_assignment + set_api_key + set_course_and_assignment + end + + def set_api_key + api_key_value = request.headers['X-API-KEY'] + APIKeysHandler.api_key = ApiKey.find_by(value: api_key_value) + end + + def set_course_and_assignment(assignment_id = params[:assignment_id]) + assignment = Assignment.find_by(id: assignment_id) + return nil if assignment.nil? + + APIKeysHandler.course = assignment.course + assignment + end + + def render_submission_similarities + assignment = set_course_and_assignment(params[:assignment_id]) + + if assignment.nil? + render json: { error: 'Assignment does not exist' }, status: :bad_request + return + end + # Check if the assignment has associated submission files. + if assignment.submissions.empty? + render json: { status: 'empty' }, status: :ok + return + end + + # Determine process status of assignment + submission_similarity_process = assignment.submission_similarity_process + case submission_similarity_process.status + when SubmissionSimilarityProcess::STATUS_RUNNING, SubmissionSimilarityProcess::STATUS_WAITING + render json: { status: 'processing' }, status: :ok + return + when SubmissionSimilarityProcess::STATUS_ERRONEOUS + render json: { status: 'error', message: 'SSID is busy or under maintenance. Please try again later.' }, + status: :service_unavailable + return + end + + submission_similarities = assignment.submission_similarities + + ### Filtering Code + # Apply the threshold filter + if params[:threshold].present? + threshold_value = params[:threshold].to_f + submission_similarities = submission_similarities.where('similarity >= ?', threshold_value) + end + + # Apply the limit filter + if params[:limit].present? + limit_value = params[:limit].to_i + submission_similarities = submission_similarities.limit(limit_value) + end + + # Apply the page filter + if params[:page].present? + per_page = params[:limit].present? ? limit_value : 20 # Default per page value is 20, limit to use a page size + page_number = params[:page].to_i + submission_similarities = submission_similarities.offset(per_page * (page_number - 1)) + end + + # Process subnission similarities into readable format for returning via JSON + result_submission_similarities = [] + + submission_similarities.each { |submission_similarity| + result_submission_similarities.append( { + submissionSimilarityID: submission_similarity.id, + student1ID: submission_similarity.submission1.student_id, + student2ID: submission_similarity.submission2.student_id, + similarity: submission_similarity.similarity + } + ) + } + + render json: { status: 'processed', submissionSimilarities: result_submission_similarities }, status: :ok + end + + def render_pair_of_flagged_submissions + submission_similarity = SubmissionSimilarity.find_by( + assignment_id: params[:assignment_id], + id: params[:id] + ) + + if submission_similarity.nil? + render json: { error: 'Submission similarities requested do not exist.' }, status: :bad_request + return + end + + matches = [] + + submission_similarity.similarity_mappings.each do |similarity| + matches.append( + { + student1StartLine: similarity.start_line1 + 1, + student1EndLine: similarity.end_line1 + 1, + student2StartLine: similarity.start_line2 + 1, + student2EndLine: similarity.end_line2 + 1, + numOfMatchingStatements: similarity.statement_count + } + ) + end + + pdf_file_path = "api/v1/assignments/#{submission_similarity.assignment_id}/" \ + "submission_similarities/#{submission_similarity.id}/view_pdf" + + render json: { + similarity: submission_similarity.similarity, + matches: matches, + pdf_link: pdf_file_path + }, status: :ok + end + + def generate_pdf + submission_similarity = SubmissionSimilarity.find_by( + assignment_id: params[:assignment_id], + id: params[:id] + ) + + if submission_similarity.nil? + render json: { error: 'Submission similarities requested do not exist.' }, status: :bad_request + return + end + + pdf_content = generate_pdf_content(submission_similarity) + send_data pdf_content, type: 'application/pdf', + disposition: 'attachment', filename: "#{submission_similarity.id}.pdf" + end + + def generate_pdf_content(submission_similarity) + @submission_similarity = submission_similarity + html_content = render_to_string(template: 'api/v1/submission_similarities/report_template', layout: false) + pdf_report = PDFKit.new(html_content) + pdf_report.to_pdf + end + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1199e1c8..39c6b8f6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -17,24 +17,9 @@ class ApplicationController < ActionController::Base protect_from_forgery - before_action :authorize + before_action :authenticate_user! before_action :sanitize_id, only: [:index, :show, :edit, :update, :destroy] - protected - - def authorize - # get user and respective membership - @user = User.find_by_id(session[:user_id]) - - unless @user - # redirect_to login_url, notice: "Please log in" - redirect_to cover_url - - else - @membership = UserCourseMembership.find_by_user_id(@user.id) - end - end - private def sanitize_id @@ -72,7 +57,7 @@ def sanitize_id # } def authenticate_actions_for_role(role, opts={}) - return if @user.is_admin + return if current_user.is_admin # Sanitize raise unless opts[:course] @@ -82,7 +67,7 @@ def authenticate_actions_for_role(role, opts={}) course = opts[:course] # Get current user's role - membership = course.membership_for_user(@user) + membership = course.membership_for_user(current_user) # Check if we need to authenticate if membership.nil? or (membership and membership.role == role and !opts[:only].include? action_name.intern) @@ -92,12 +77,12 @@ def authenticate_actions_for_role(role, opts={}) end def authenticate_custom_actions_for_teaching_staff(opts) - return if @user.is_admin + return if current_user.is_admin # Sanitize raise unless opts[:only] - unless @user.is_staff_or_ta? and opts[:only].include? action_name.intern + unless current_user.is_staff_or_ta? and opts[:only].include? action_name.intern redirect_to( { controller: "announcements", action: "index" }, alert: "You do not have access to the url \"#{request.env['REQUEST_URI']}\". Please contact the administrator for more information.") return end @@ -107,7 +92,7 @@ def authenticate_actions_for_admin(opts) raise unless opts[:only] # Check if we need to authenticate - unless @user.is_admin + unless current_user.is_admin if opts[:only].include? action_name.intern redirect_to( { controller: "announcements", action: "index" }, alert: "You do not have access to the url \"#{request.env['REQUEST_URI']}\". Please contact the administrator for more information.") end @@ -121,14 +106,14 @@ def authenticate_actions_for_admin_or_teaching_staff(opts) user_current_role = nil # Get course - if (opts[:course] != nil && !@user.is_admin) + if (opts[:course] != nil && !current_user.is_admin) course = opts[:course] # check current user role (if any) - user_current_role = course.membership_for_user(@user).role + user_current_role = course.membership_for_user(current_user).role end # Check if we need to authenticate - unless @user.is_admin or user_current_role == UserCourseMembership::ROLE_TEACHING_STAFF + unless current_user.is_admin or user_current_role == UserCourseMembership::ROLE_TEACHING_STAFF if opts[:only].include? action_name.intern redirect_to( { controller: "announcements", action: "index" }, alert: "You do not have access to the url \"#{request.env['REQUEST_URI']}\". Please contact the administrator for more information.") end diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index e99adf58..955121bd 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -28,10 +28,10 @@ class CoursesController < ApplicationController if @course controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_TEACHING_STAFF, course: @course, - only: [ :index, :cluster_students , :status] + only: [ :index, :cluster_students , :status ] controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_TEACHING_ASSISTANT, course: @course, - only: [ :index, :cluster_students , :status] + only: [ :index, :cluster_students , :status ] controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, course: @course, only: [ :index ] @@ -49,7 +49,7 @@ def status # GET /courses def index - @courses = @user.is_admin ? Course.all : @user.courses + @courses = current_user.is_admin ? Course.all : current_user.courses end # GET /courses/new @@ -73,25 +73,22 @@ def create c.expiry = Time.zone.local_to_utc(DateTime.new(expiry_date.year, expiry_date.month, expiry_date.mday, expiry_time.hour, expiry_time.min)) } - - if @course.errors.empty? and @course.save - + if @course.valid? and @course.save else render action: "new" + return end - if not @user.is_admin - @membership = UserCourseMembership.new { |m| - m.user = @user - m.course = @course - m.role = UserCourseMembership::ROLE_TEACHING_STAFF - } + @membership = UserCourseMembership.new { |m| + m.user = current_user + m.course = @course + m.role = UserCourseMembership::ROLE_TEACHING_STAFF + } - if @membership.errors.empty? and @membership.save - redirect_to courses_url, notice: 'Course was successfully created.' - else - render action: "new" - end + if @membership.valid? && @membership.save + redirect_to courses_url, notice: 'Course was successfully created.' + else + render action: 'new' end end @@ -137,4 +134,6 @@ def cluster_students } end end + end + diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb deleted file mode 100644 index a375e5dc..00000000 --- a/app/controllers/password_resets_controller.rb +++ /dev/null @@ -1,89 +0,0 @@ -class PasswordResetsController < ApplicationController - skip_before_action :authorize - - def forget_password - end - - def send_password_reset_link - @user_email = params["user_email"] - - user = User.find_by_email(@user_email) - - token = PasswordResetsHelper.generate_reset_token() - if user - password_reset = PasswordReset.find_by_user_id(user.id) - - if password_reset.nil? - password_reset = PasswordReset.new { |pw| - pw.user_id = user.id - pw.token = token - } - else - # In case user makes another request to reset password, update token and send email again. - password_reset.token = token - end - - if password_reset.valid? - password_reset.save - - reset_link = (Rails.env == "production" ? url_for(:protocol => "#{Rails.application.config.protocol}", :host => "#{Rails.application.config.host}", :action => "reset_password", :token => "#{token}").to_s - : url_for(:protocol => "#{Rails.application.config.protocol}", :host => "#{Rails.application.config.host}", :port => "3000", :action => "reset_password", :token => "#{token}").to_s) - end - end - - begin - if user - UserMailer.reset_link(@user_email, reset_link).deliver_now - elsif UsersHelper.is_valid_email?(@user_email) - UserMailer.reset_link_non_existent_account(@user_email).deliver_now - else - # do nothing - end - rescue => exception - logger.error("Failed to deliver mail to: #{@user_email} with message: #{exception.message}") - trace = exception.backtrace.join("\n") - logger.error(trace) - end - - render 'send_password_reset_link' - end - - def reset_password - @user_token = params["token"] - @valid_token = is_valid_token(@user_token) - end - - def update_password - user_token = params["user_token"] - @user = User.joins(:password_resets).where(:password_resets => {:token => user_token}).first - - UsersHelper.validate_password(@user, params["new_password"], params["confirm_new_password"]) - - @user.password_digest = BCrypt::Password.create(params["new_password"]) if @user.errors.empty? - - if @user.errors.empty? && @user.save - # Remove token record in db upon updating user successfully - password_reset = PasswordReset.find_by_user_id(@user.id) - password_reset.destroy - - reset_time = Time.now - UserMailer.password_reset(@user.email, reset_time).deliver_now - - redirect_to login_url, notice: 'User password was successfully reset.' - else - redirect_to login_url, alert: 'User password failed to be reset.' - end - end - - private - - def is_valid_token(token) - now = Time.now.getutc - - # Token will be expired after 30 minutes counting from its creation time - password_resets = PasswordReset.where(["token = ? AND updated_at > ? AND updated_at < ? ", token, now - 30.minutes, now]) - return password_resets.length > 0 - end - - -end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb deleted file mode 100644 index ad6e40e5..00000000 --- a/app/controllers/sessions_controller.rb +++ /dev/null @@ -1,61 +0,0 @@ -=begin -This file is part of SSID. - -SSID is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SSID is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License -along with SSID. If not, see . -=end - -class SessionsController < ApplicationController - skip_before_action :authorize - - def index - render "layouts/cover" - end - - def new - end - #orRgKyGUs7cz - def create - user = User.find_by_name(params[:name]) - - if user and user.authenticate(params[:password]) and user.is_admin_approved - if user.activated? - session[:user_id] = user.id - redirect_to root_url - else - redirect_to login_url, alert: "Your account is not activated. Please check your email for the activation link." - end - else - redirect_to login_url, alert: "Invalid user/password combination or user account is still being processed." - end - end - - def check_hash - # obtain the hash - input_hash = params[:id] - # verify the hash - guest_user = GuestUsersDetail.find_by_hash_string(params[:id]) - # allow access if the hash is verified and else redirect to login page - if guest_user - session[:user_id] = guest_user.user_id - redirect_to root_url - else - redirect_to login_url, alert: "The hash is not valid. Please use a valid sharable link or log in with your credientals." - end - end - - def destroy - session[:user_id] = nil - redirect_to cover_url - end -end diff --git a/app/controllers/submission_logs_controller.rb b/app/controllers/submission_logs_controller.rb index 164c380c..43677b37 100644 --- a/app/controllers/submission_logs_controller.rb +++ b/app/controllers/submission_logs_controller.rb @@ -41,18 +41,18 @@ def index def view_similarity @assignment = Assignment.find(params[:assignment_id]) - unless @user.is_admin + unless current_user.is_admin course = @assignment.course - membership = course.membership_for_user(@user) - has_guest_detail = GuestUsersDetail.find_by_user_id_and_assignment_id(@user.id, @assignment.id) + membership = course.membership_for_user(current_user) + has_guest_detail = GuestUsersDetail.find_by_user_id_and_assignment_id(current_user.id, @assignment.id) if membership.nil? # Create a guest membership for current user - CoursesService.create_guest_membership(course, @user) - CoursesService.create_guest_detail_entry(@assignment, @user) + CoursesService.create_guest_membership(course, current_user) + CoursesService.create_guest_detail_entry(@assignment, current_user) else if membership.role == UserCourseMembership::ROLE_GUEST && has_guest_detail.nil? - CoursesService.create_guest_detail_entry(@assignment, @user) + CoursesService.create_guest_detail_entry(@assignment, current_user) end end end diff --git a/app/controllers/submission_similarities_controller.rb b/app/controllers/submission_similarities_controller.rb index 1a66523d..92eb26ad 100644 --- a/app/controllers/submission_similarities_controller.rb +++ b/app/controllers/submission_similarities_controller.rb @@ -42,10 +42,22 @@ def index @hashed_url = session[:hashed_url] @displayDialog = session[:displayDialog] session[:displayDialog] = false - - @submission_similarities = SubmissionSimilarity.where( - assignment_id: @assignment.id - ).order('similarity desc').paginate(page: params[:page], per_page: 20) + + # Determine sort direction + sort_direction = params[:sort_direction] || 'descending' + order_string = sort_direction == 'ascending' ? 'status asc' : 'status desc' + + page_size = params[:page_size] || 20 + if params[:sort_direction] && params[:sort_direction] != 'default' + @submission_similarities = SubmissionSimilarity.where( + assignment_id: @assignment.id + ).order(order_string, 'similarity desc').paginate(page: params[:page], per_page: page_size) + else + @submission_similarities = SubmissionSimilarity.where( + assignment_id: @assignment.id + ).order('(similarity_1_to_2 + similarity_2_to_1)/2 desc, Greatest(similarity_1_to_2, similarity_2_to_1) desc') + .paginate(page: params[:page], per_page: page_size) + end end def create_guest_user @@ -149,13 +161,13 @@ def confirm_as_plagiarism SubmissionLog.create { |sl| sl.submission_similarity = @submission_similarity sl.submission = @submission_similarity.submission1 - sl.marker = @user + sl.marker = current_user sl.log_type = SubmissionLog::TYPE_PAIR_CONFIRM_AS_PLAGIARISM } SubmissionLog.create { |sl| sl.submission_similarity = @submission_similarity sl.submission = @submission_similarity.submission2 - sl.marker = @user + sl.marker = current_user sl.log_type = SubmissionLog::TYPE_PAIR_CONFIRM_AS_PLAGIARISM } end @@ -172,13 +184,13 @@ def suspect_as_plagiarism SubmissionLog.create { |sl| sl.submission_similarity = @submission_similarity sl.submission = @submission_similarity.submission1 - sl.marker = @user + sl.marker = current_user sl.log_type = SubmissionLog::TYPE_PAIR_SUSPECT_AS_PLAGIARISM } SubmissionLog.create { |sl| sl.submission_similarity = @submission_similarity sl.submission = @submission_similarity.submission2 - sl.marker = @user + sl.marker = current_user sl.log_type = SubmissionLog::TYPE_PAIR_SUSPECT_AS_PLAGIARISM } end @@ -195,13 +207,13 @@ def unmark_as_plagiarism SubmissionLog.create { |sl| sl.submission_similarity = @submission_similarity sl.submission = @submission_similarity.submission1 - sl.marker = @user + sl.marker = current_user sl.log_type = SubmissionLog::TYPE_PAIR_UNMARK_AS_PLAGIARISM } SubmissionLog.create { |sl| sl.submission_similarity = @submission_similarity sl.submission = @submission_similarity.submission2 - sl.marker = @user + sl.marker = current_user sl.log_type = SubmissionLog::TYPE_PAIR_UNMARK_AS_PLAGIARISM } end @@ -217,7 +229,7 @@ def mark_student_as_not_guilty(submission, submission_similarity) SubmissionLog.create { |sl| sl.submission_similarity = submission_similarity sl.submission = submission - sl.marker = @user + sl.marker = current_user sl.log_type = SubmissionLog::TYPE_STUDENT_MARK_AS_NOT_GUILTY } end diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 2217a965..2d2d7f00 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -56,7 +56,7 @@ def mark_as_guilty SubmissionLog.create { |sl| sl.submission_similarity = @submission_similarity sl.submission = @submission - sl.marker = @user + sl.marker = current_user sl.log_type = SubmissionLog::TYPE_STUDENT_MARK_AS_GUILTY } end @@ -70,7 +70,7 @@ def mark_as_not_guilty SubmissionLog.create { |sl| sl.submission_similarity = @submission_similarity sl.submission = @submission - sl.marker = @user + sl.marker = current_user sl.log_type = SubmissionLog::TYPE_STUDENT_MARK_AS_NOT_GUILTY } end diff --git a/app/controllers/user_course_memberships_controller.rb b/app/controllers/user_course_memberships_controller.rb index c26cb56a..cfa017b8 100644 --- a/app/controllers/user_course_memberships_controller.rb +++ b/app/controllers/user_course_memberships_controller.rb @@ -16,4 +16,85 @@ =end class UserCourseMembershipsController < ApplicationController + before_action { |controller| + if params[:course_id] + @course = Course.find(params[:course_id]) + end + + if @course + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_TEACHING_STAFF, + course: @course, + only: [ :index, :new, :create, :edit, :update, :destroy ] + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_TEACHING_ASSISTANT, + course: @course, + only: [ :index ] + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, + course: @course, + only: [ ] + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_GUEST, + course: @course, + only: [ ] + end + } + + # GET /courses/:course_id/user_course_memberships + def index + @course = Course.find(params[:course_id]) + @staff = @course.staff + @teaching_assistants = @course.teaching_assistants + @students = @course.students + @guests = @course.guests + end + + # GET /courses/:course_id/user_course_memberships/new + def new + @course = Course.find_by_id(params[:course_id]) + @user_course_membership = UserCourseMembership.new + end + + # POST /courses/:course_id/user_course_memberships + def create + @user = User.find_by_email(params[:user_course_membership][:user_email]) + + @course = Course.find_by_id(params[:course_id]) + course_role = params[:user_course_membership][:course_role] + @user_course_membership = UserCourseMembership.new { |ucm| + ucm.course = @course + ucm.user = @user + ucm.role = course_role + } + + if @user_course_membership.save + return redirect_to course_user_course_memberships_url, notice: "User was successfully added to the course." + else + return render action: "new" + end + end + + # GET /courses/:course_id/user_course_memberships/:id/edit + def edit + @course = Course.find_by_id(params[:course_id]) + @user_course_membership = UserCourseMembership.find_by_id(params[:id]) + end + + # PATCH /courses/:course_id/user_course_memberships/:id + def update + @user_course_membership = UserCourseMembership.find_by_id(params[:id]) + course_role = params[:user_course_membership][:course_role] + @user_course_membership.role = course_role + if @user_course_membership.save + return redirect_to course_user_course_memberships_url, notice: "User course role was successfully updated." + else + return render action: "edit" + end + end + + # DELETE /courses/:course_id/user_course_memberships/:id + def destroy + @user_course_membership = UserCourseMembership.find_by_id(params[:id]) + @user_course_membership.destroy + + redirect_to course_user_course_memberships_url, notice: "User was successfully removed from the course." + end end + diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb new file mode 100644 index 00000000..a88a9867 --- /dev/null +++ b/app/controllers/users/confirmations_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class Users::ConfirmationsController < Devise::ConfirmationsController + # GET /resource/confirmation/new + def new + super + + end + + # POST /resource/confirmation + def create + super + end + + # GET /resource/confirmation?confirmation_token=abcdef + # def show + # super + # end + + # protected + + # The path used after resending confirmation instructions. + # def after_resending_confirmation_instructions_path_for(resource_name) + # super(resource_name) + # end + + # The path used after confirmation. + # def after_confirmation_path_for(resource_name, resource) + # super(resource_name, resource) + # end +end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb new file mode 100644 index 00000000..7887e3b4 --- /dev/null +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController + # You should configure your model like this: + # devise :omniauthable, omniauth_providers: [:twitter] + + # You should also create an action method in this controller like this: + # def twitter + # end + + # More info at: + # https://github.com/heartcombo/devise#omniauth + + # GET|POST /resource/auth/twitter + # def passthru + # super + # end + + # GET|POST /users/auth/twitter/callback + # def failure + # super + # end + + # protected + + # The path used when OmniAuth fails + # def after_omniauth_failure_path_for(scope) + # super(scope) + # end + + def google_oauth2 + user = User.from_omniauth(auth) + # byebug + if user.present? + found_user = User.find_by(email: auth.info.email) + if found_user.present? + if found_user.is_admin_approved? + sign_out_all_scopes + flash[:success] = t 'devise.omniauth_callbacks.success', kind: 'Google' + sign_in_and_redirect user, event: :authentication + else + redirect_to new_user_session_url, alert: "Your account is still being processed." + end + else + user.name = user.full_name + user.confirm + user.save + redirect_to root_url, notice: "Welcome to SSID! Your account is being processed. An email will be sent to you once your account is approved." + end + else + flash[:alert] = + t 'devise.omniauth_callbacks.failure', kind: 'Google', reason: "#{auth.info.email} is not authorized." + redirect_to new_user_session_path + end + end + protected + def after_omniauth_failure_path_for(_scope) + new_user_session_path + end + def after_sign_in_path_for(resource_or_scope) + stored_location_for(resource_or_scope) || root_path + end + private + def auth + @auth ||= request.env['omniauth.auth'] + end +end diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb new file mode 100644 index 00000000..259dbb08 --- /dev/null +++ b/app/controllers/users/passwords_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class Users::PasswordsController < Devise::PasswordsController + # GET /resource/password/new + # def new + # super + # end + + # POST /resource/password + # def create + # super + # end + + # GET /resource/password/edit?reset_password_token=abcdef + # def edit + # super + # end + + # PUT /resource/password + # def update + # super + # end + + # protected + + # def after_resetting_password_path_for(resource) + # super(resource) + # end + + # The path used after sending reset password instructions + # def after_sending_reset_password_instructions_path_for(resource_name) + # super(resource_name) + # end +end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb new file mode 100644 index 00000000..7a235437 --- /dev/null +++ b/app/controllers/users/registrations_controller.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +class Users::RegistrationsController < Devise::RegistrationsController + before_action :configure_sign_up_params, only: [:create] + before_action :configure_account_update_params, only: [:update] + + # GET /resource/sign_up + def new + super + end + + # POST /resource + # def create + # super + # end + + # GET /resource/edit + # def edit + # super + # end + + # PUT /resource + # def update + # super + # end + + # DELETE /resource + # def destroy + # super + # end + + # GET /resource/cancel + # Forces the session data which is usually expired after sign + # in to be expired now. This is useful if the user wants to + # cancel oauth signing in/up in the middle of the process, + # removing all OAuth session data. + # def cancel + # super + # end + + def update_resource(resource, params) + if resource.provider == 'google_oauth2' + params.delete('current_password') + resource.password = params['password'] + resource.update_without_password(params) + else + resource.update_with_password(params) + end + end + + protected + + # If you have extra params to permit, append them to the sanitizer. + def configure_sign_up_params + devise_parameter_sanitizer.permit(:sign_up, keys: [ :name, :full_name]) + end + + # If you have extra params to permit, append them to the sanitizer. + def configure_account_update_params + devise_parameter_sanitizer.permit(:account_update, keys: [:name, :full_name]) + end + + # The path used after sign up. + # def after_sign_up_path_for(resource) + # super(resource) + # end + + # The path used after sign up for inactive accounts. + # def after_inactive_sign_up_path_for(resource) + # super(resource) + # end +end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb new file mode 100644 index 00000000..9ff4682e --- /dev/null +++ b/app/controllers/users/sessions_controller.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +class Users::SessionsController < Devise::SessionsController + # before_action :configure_sign_in_params, only: [:create] + + # GET /resource/sign_in + # def new + # super + # end + + # POST /resource/sign_in + # def create + # super + # end + + # DELETE /resource/sign_out + # def destroy + # super + # end + + + skip_before_action :authenticate_user!, only: [:new, :create, :index, :guide] + + def index + render "layouts/cover" + end + + def new + super + end + + def create + user = User.find_by_email(params[:user][:email]) + if user and user.valid_password?(params[:user][:password]) and user.is_admin_approved + if user.confirmed? + sign_in user + redirect_to root_url + else + redirect_to new_user_session_url, alert: "Your account is not activated. Please check your email for the activation link." + end + else + redirect_to new_user_session_url, alert: "Invalid user/password combination or user account is still being processed." + end + end + + + def destroy + sign_out current_user + redirect_to root_url + end + + def guide + render "layouts/guide" + end + + protected + + # # If you have extra params to permit, append them to the sanitizer. + # def configure_sign_in_params + # devise_parameter_sanitizer.permit(:sign_in, keys: [:email, :password, :remember_me]) + # end + + def after_sign_out_path_for(_resource_or_scope) + new_user_session_path + end + def after_sign_in_path_for(resource_or_scope) + stored_location_for(resource_or_scope) || root_path + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb deleted file mode 100644 index 56324ead..00000000 --- a/app/controllers/users_controller.rb +++ /dev/null @@ -1,105 +0,0 @@ -=begin -This file is part of SSID. - -SSID is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SSID is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License -along with SSID. If not, see . -=end - -class UsersController < ApplicationController - skip_before_action :authorize, only: [:new, :create] - before_action { |controller| - if params[:course_id] - @course = Course.find(params[:course_id]) - controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, - course: @course, - only: [ ] - controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_GUEST, - course: @course, - only: [ ] - end - } - - # GET /signup - def new - @user = User.new - end - - def create - @user = User.new(user_params) - if @user.save - UserMailer.account_activation(@user).deliver_now - flash[:notice] = "Please check your email to activate your account." - redirect_to root_url - else - render 'new' - end - end - - - - - - # GET /courses/1/users - def index - @course = Course.find(params[:course_id]) - @users = @course.users - @staff = @course.staff - @teaching_assistants = @course.teaching_assistants - @students = @course.students - @guests = @course.guests - end - - # GET /users/1/edit - def edit - @user = User.find(params[:id]) - end - - # PUT /users/1 - def update - @user = User.find(params[:id]) - - # Check for new password unless admin and do not wish to change password - unless @user.is_admin and params[:user]["new_password"].strip.empty? - UsersHelper.validate_password(@user, params[:user]["new_password"], params[:user]["confirm_new_password"]) - end - - # Check for old password - unless @user.is_admin - @user.errors.add :old_password, "is incorrect" unless @user.authenticate(params[:user]["old_password"]) - @user.errors.add :new_password, "must be different from the old password" if params[:user]["new_password"] == params[:user]["old_password"] - end - - # Check for email - UsersHelper.validate_email(@user, params[:user]["email"]) - - # Update user - unless @user.is_admin and params[:user]["new_password"].strip.empty? - @user.password_digest = BCrypt::Password.create(params[:user]["new_password"]) if @user.errors.empty? - end - if @user.is_admin - @user.full_name = params[:user]["full_name"] - @user.name = params[:user]["name"] - end - - if @user.errors.empty? and @user.save - redirect_to edit_user_url(@user), notice: 'User settings were successfully updated.' - else - render action: "edit" - end - end - - private - def user_params - params.require(:user).permit(:name, :email, :password, :password_confirmation, :full_name) - end -end diff --git a/app/helpers/account_activations_helper.rb b/app/helpers/account_activations_helper.rb deleted file mode 100644 index c4d5ac72..00000000 --- a/app/helpers/account_activations_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module AccountActivationsHelper -end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 89edb92f..dee92325 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -5,7 +5,7 @@ module ApplicationHelper EMAIL_DEFAULT_SENDER = "no-reply@ssid.comp.nus.edu.sg" # copyright - COPYRIGHT_DATE_RANGE = "2009–2022" + COPYRIGHT_DATE_RANGE = '2009–2023'.freeze COPYRIGHT_HOLDER = "Web Information Retrieval and Natural Language Processing Group" def self.is_application_healthy diff --git a/app/helpers/password_resets_helper.rb b/app/helpers/password_resets_helper.rb deleted file mode 100644 index eefadbe6..00000000 --- a/app/helpers/password_resets_helper.rb +++ /dev/null @@ -1,28 +0,0 @@ -=begin -This file is part of SSID. - -SSID is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SSID is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License -along with SSID. If not, see . -=end - -require 'digest' - -module PasswordResetsHelper - private - - def self.generate_reset_token - random_input = SecureRandom.base64() - hash = Digest::SHA512.hexdigest(random_input) - end - -end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb deleted file mode 100644 index c83a4770..00000000 --- a/app/helpers/users_helper.rb +++ /dev/null @@ -1,22 +0,0 @@ -module UsersHelper - private - - def self.validate_password(user, password, confirm_password) - user.errors.add :password, "cannot be blank" if password.empty? - user.errors.add :password, "must be at least #{User::MIN_PASSWORD_LENGTH} characters long" if password.length < User::MIN_PASSWORD_LENGTH - user.errors.add :password, "must contain at least 1 lower case character" if (password =~ /[a-z]+/).nil? - user.errors.add :password, "must contain at least 1 upper case character" if (password =~ /[A-Z]+/).nil? - user.errors.add :password, "must contain at least 1 digit or special character" if (password =~ /[0-9~!@#$%^&*()+=|]+/).nil? - user.errors.add :confirm_password, "does not match password" if password != confirm_password - end - - def self.validate_email(user, email) - user.errors.add :email, "cannot be blank" if email.empty? - user.errors.add :email, "can contain alphanumeric, _ (underscore), - (hyphen), and . (dot) characters" unless is_valid_email?(email) - end - - def self.is_valid_email?(email) - email_pattern = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,4}$/ - is_valid_email = (email =~ email_pattern) - end -end \ No newline at end of file diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 73f78aa7..19067a81 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -1,27 +1,5 @@ class UserMailer < ApplicationMailer default from: ApplicationHelper::EMAIL_DEFAULT_SENDER - - def reset_link(user_email, reset_link) - @reset_link = reset_link - mail(to: user_email, subject: "[SSID] Reset your password") - end - - def reset_link_non_existent_account(user_email) - mail(to: user_email, subject: "[SSID] Reset your password") - end - - def password_reset(user_email, reset_time) - @user_email = user_email - @reset_time = reset_time - mail(to: user_email, subject: "[SSID] Your SSID password has been reset.") - end - - def account_activation(user) - @user = user - @user_email = user.email - @user_name = user.name - mail(to: @user_email, subject: "[SSID] Activate your account") - end def admin_approved(user) @user = user diff --git a/app/models/api_key.rb b/app/models/api_key.rb new file mode 100644 index 00000000..9fbba6f1 --- /dev/null +++ b/app/models/api_key.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ApiKey < ApplicationRecord + belongs_to :user + + validates :value, uniqueness: true # , presence: true + + # before_validation :generate_value + + # private + + # def generate_value + # self.value ||= SecureRandom.hex(32) + # end +end diff --git a/app/models/password_reset.rb b/app/models/password_reset.rb deleted file mode 100644 index 964025b6..00000000 --- a/app/models/password_reset.rb +++ /dev/null @@ -1,3 +0,0 @@ -class PasswordReset < ApplicationRecord - belongs_to :user -end diff --git a/app/models/user.rb b/app/models/user.rb index a335f6c5..fdbc86c1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -16,6 +16,11 @@ =end class User < ActiveRecord::Base + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :database_authenticatable, :registerable, + :recoverable, :rememberable, :validatable, :confirmable, + :omniauthable, :omniauth_providers => [:google_oauth2] MIN_PASSWORD_LENGTH = 8 has_many :memberships , class_name: "UserCourseMembership", :dependent => :delete_all @@ -23,18 +28,15 @@ class User < ActiveRecord::Base has_many :courses, -> { distinct }, :through => :memberships has_many :assignments, -> { distinct }, :through => :courses has_many :submissions, foreign_key: "student_id" - has_many :password_resets, class_name: "PasswordReset" + has_many :api_keys, :dependent => :destroy - validates :name, :password_digest, presence: true + validates :name, presence: true validates :name, uniqueness: true validates :email, presence: true, uniqueness: true, format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i } + validate :password_complexity - has_secure_password before_destroy :ensure_an_admin_remains - attr_accessor :activation_token - before_create :create_activation_digest - def is_some_staff? self.courses.any? { |c| c.membership_for_user(self).role == UserCourseMembership::ROLE_TEACHING_STAFF } end @@ -48,10 +50,24 @@ def full_name the_full_name.strip.empty? ? nil : the_full_name end - def authenticated?(attribute, token) - digest = send("#{attribute}_digest") - return false if digest.nil? - BCrypt::Password.new(digest).is_password?(token) + def active_for_authentication? + super && self.is_admin_approved? + end + + def inactive_message + if !self.is_admin_approved? + :not_approved + else + super # Use whatever other message + end + end + + def self.from_omniauth(auth) + where(provider: auth.provider, uid: auth.uid).first_or_create do |user| + user.email = auth.info.email + user.password = Devise.friendly_token[0, 20] + user.full_name = auth.info.name # assuming the user model has a name + end end private @@ -65,19 +81,12 @@ def ensure_an_admin_remains end end - def User.digest(string) - cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost - BCrypt::Password.create(string, cost: cost) - end - - def User.new_token - SecureRandom.urlsafe_base64 - end + def password_complexity + # Regexp extracted from https://stackoverflow.com/questions/19605150/regex-for-password-must-contain-at-least-eight-characters-at-least-one-number-a + return if password.blank? || password =~ /(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-])/ - def create_activation_digest - self.activation_token = User.new_token - self.activation_digest = User.digest(activation_token) + errors.add :password, 'Complexity requirement not met. Please use: 1 uppercase, 1 lowercase, 1 digit and 1 special character' end - end + diff --git a/app/models/user_course_membership.rb b/app/models/user_course_membership.rb index ca04df3f..d862d88b 100644 --- a/app/models/user_course_membership.rb +++ b/app/models/user_course_membership.rb @@ -32,8 +32,10 @@ class UserCourseMembership < ActiveRecord::Base belongs_to :user belongs_to :course + validate :is_existing_user validates_uniqueness_of :user_id, scope: [ :course_id ] + def role_string ROLE_STRINGS[self.role] end @@ -43,4 +45,16 @@ def self.options_for_non_student_roles role_id == ROLE_STUDENT } end + + def user_email + self.user.nil? ? "" : self.user.email + end + + def is_existing_user + if self.user.nil? + errors.add :user_email, "This user email is not in SSID. Please invite him or her to signup." + return false + end + end + end diff --git a/app/views/admin/users/_edit_form.html.erb b/app/views/admin/users/_edit_form.html.erb deleted file mode 100644 index 5848a1e0..00000000 --- a/app/views/admin/users/_edit_form.html.erb +++ /dev/null @@ -1,48 +0,0 @@ -<%= form_for([:admin, @the_user]) do |f| %> - <% if @the_user.errors.any? %> -
-
<%= pluralize(@the_user.errors.count, "error") %> prohibited this user from being saved:
- -
    - <% @the_user.errors.full_messages.each do |msg| %> -
  • <%= msg %>
  • - <% end %> -
-
- <% end %> -
- <%= f.label :full_name, class: "form-label" %> - <%= f.text_field :full_name, class: "form-control" %> -
- <% if @course %> -
- <%= hidden_field_tag :user_course_id, @course.id, name: "user[course_id]" %> -
- <% unless @membership.role == UserCourseMembership::ROLE_STUDENT %> -
- <%= f.label :course_role, class: "form-label" %> - <%= select_tag "user_course_role", options_for_select(UserCourseMembership.options_for_non_student_roles, @membership.role ), name: "user[course_role]", class: "form-select" %> -
- <% end %> - <% end %> -
- <%= f.label :name, "Name", class: "form-label" %> - <%= f.text_field :name, class: "form-control" %> -
-
- <%= f.label :email, "Email", class: "form-label" %> - <%= f.email_field :email, class: "form-control" %> -
-
- <%= f.label :new_password, class: "form-label" %> - <%= f.password_field :new_password, class: "form-control" %> - Leave blank to keep unchanged -
-
- <%= f.label :confirm_new_password, class: "form-label" %> - <%= f.password_field :confirm_new_password, class: "form-control" %> -
-
- <%= f.submit class: "submit" %> -
-<% end %> diff --git a/app/views/admin/users/_form.html.erb b/app/views/admin/users/_form.html.erb deleted file mode 100644 index 91b6a53e..00000000 --- a/app/views/admin/users/_form.html.erb +++ /dev/null @@ -1,58 +0,0 @@ -<%= form_for([:admin, @the_user]) do |f| %> - <% if @the_user.errors.any? %> -
-
<%= pluralize(@the_user.errors.count, "error") %> prohibited this user from being saved:
- -
    - <% @the_user.errors.full_messages.each do |msg| %> -
  • <%= msg %>
  • - <% end %> -
-
- <% end %> - <% if @course %> -
- If SSID has an existing user with the provided ID (Name), SSID will add that user to the course with the specified role but ignore all the other fields in this form. -
- <% end %> -
-
- <%= f.label :full_name, class: "form-label" %> - <%= f.text_field :full_name, class: "form-control" %> -
- <% if @course %> -
- <%= hidden_field_tag :user_course_id, @course.id, name: "user[course_id]" %> -
-
- <%= f.label :course_role, class: "form-label" %> - <%= select_tag "user_course_role", options_for_select(UserCourseMembership.options_for_non_student_roles), name: "user[course_role]", class: "form-select" %> -
- <% end %> -
- <%= f.label :name, "Name", class: "form-label" %> - <%= f.text_field :name, class: "form-control" %> -
-
- <%= f.label :email, "Email", class: "form-label" %> - <%= f.email_field :email, class: "form-control" %> -
- <% if action_name == "new" || action_name == "create" %> -
- <%= f.label :password, class: "form-label" %> - <%= f.password_field :password, class: "form-control" %> -
-
- <%= f.label :confirm_password, class: "form-label" %> - <%= f.password_field :confirm_password, class: "form-control" %> -
- <% end %> -
- <% if @course.nil? %> - <%= f.submit "Create Admin User", class: "submit" %> - <% else %> - <%= f.submit "Add #{@course.code} User", class: "submit" %> - <% end %> -
-
-<% end %> diff --git a/app/views/admin/users/edit.html.erb b/app/views/admin/users/edit.html.erb deleted file mode 100644 index 62b16ef7..00000000 --- a/app/views/admin/users/edit.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -
-<% if @course %> -

Edit <%= @course.code %> User

-<% else %> -

Edit Admin User

-<% end %> - -<%= render 'edit_form' %> -
\ No newline at end of file diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb index 20879310..209c89f1 100644 --- a/app/views/admin/users/index.html.erb +++ b/app/views/admin/users/index.html.erb @@ -13,7 +13,6 @@
Admins
-<%= link_to image_tag("person_add_blue_24dp.svg", size: "28", alt: "New Admin"), new_admin_user_url, {'data-toggle' => 'tooltip', 'data-placement' => 'bottom', 'title' => 'New Admin' } %>
@@ -38,10 +37,13 @@ <% if admin == @user %>
  • <%= link_to "Settings", edit_user_url(@user), class: "dropdown-item" %>
  • <% else %> + + <% end %> @@ -77,10 +79,12 @@ <%= image_tag("more_vert_black_24dp.svg", alt: "Lone Users Menu Dropdown")%> @@ -115,11 +119,14 @@ <%= image_tag("more_vert_black_24dp.svg", alt: "Lone Users Menu Dropdown")%> diff --git a/app/views/admin/users/new.html.erb b/app/views/admin/users/new.html.erb deleted file mode 100644 index ce3f4195..00000000 --- a/app/views/admin/users/new.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -
    -<% if @course.nil? %> -

    New Admin User

    -<% else %> -

    Add <%= @course.code %> User

    -<% end %> - -<%= render 'form' %> -
    \ No newline at end of file diff --git a/app/views/announcements/index.html.erb b/app/views/announcements/index.html.erb index d0a3dd0a..6497f91d 100644 --- a/app/views/announcements/index.html.erb +++ b/app/views/announcements/index.html.erb @@ -19,19 +19,20 @@ along with SSID. If not, see . <%= flash[:alert] %> <% end %> +<% if flash[:notice] %> +
    + <%= flash[:notice] %> +
    +<% end %>

    Announcements

    -<%= link_to image_tag("add_circle_outline_blue_24dp.svg", size: "28", alt: "New announcement"), new_announcement_url, { "data-toggle" => "tooltip", "data-placement" => "bottom", "title" => "New Announcement" } if @user.is_some_staff? %> +<%= link_to image_tag("add_circle_outline_blue_24dp.svg", size: "28", alt: "New announcement"), new_announcement_url, { "data-toggle" => "tooltip", "data-placement" => "bottom", "title" => "New Announcement" } if user_signed_in? and current_user.is_some_staff? %>

    -<% if flash[:notice] %> -
    - <%= flash[:notice] %> -
    -<% end %> + <% if @announcements.empty? %>
    No announcements
    @@ -40,7 +41,7 @@ along with SSID. If not, see .
    <%= @submission_similarity.submission1.student_id %><%= @submission_similarity.submission2.student_id %>Number of Matching Statements
    + Lines <%= mapping.line_range1_string %>
    + <% mapping.line_range1.each do |n| %> + <%= @submission_similarity.submission1.lines[n] %>
    + <% end %> +
    + Lines <%= mapping.line_range2_string %>
    + <% mapping.line_range2.each do |n| %> + <%= @submission_similarity.submission2.lines[n] %>
    + <% end %> +
    <%= mapping.statement_count %>
    + + + +
    + +
    +
    +

    app/controllers/admin/users_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 72 relevant lines. + 0 lines covered and + 72 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class Admin::UsersController < ApplicationController +
    36. +
      + +
      +
    37. + + + + + + +
    38. +
      + +
      +
    39. + + + + + + before_action :is_admin, only: [:index, :approve, :upgrade_to_admin_account, :destroy] +
    40. +
      + +
      +
    41. + + + + + + +
    42. +
      + +
      +
    43. + + + + + + # GET /admin/users +
    44. +
      + +
      +
    45. + + + + + + def index +
    46. +
      + +
      +
    47. + + + + + + @admins = User.where(is_admin: true) +
    48. +
      + +
      +
    49. + + + + + + @students = User.joins(:courses, :memberships).where(users: { is_admin_approved: false}).where(user_course_memberships: { role: UserCourseMembership::ROLE_STUDENT }) +
    50. +
      + +
      +
    51. + + + + + + @signups = User.where(is_admin_approved: false) - @students +
    52. +
      + +
      +
    53. + + + + + + +
    54. +
      + +
      +
    55. + + + + + + # users don't belong to any course +
    56. +
      + +
      +
    57. + + + + + + @loners = User.all - User.joins(:memberships) - @admins - @signups +
    58. +
      + +
      +
    59. + + + + + + end +
    60. +
      + +
      +
    61. + + + + + + +
    62. +
      + +
      +
    63. + + + + + + def approve +
    64. +
      + +
      +
    65. + + + + + + @the_user = User.find(params[:user_id]) +
    66. +
      + +
      +
    67. + + + + + + @the_user.update_attribute(:is_admin_approved, true) +
    68. +
      + +
      +
    69. + + + + + + @the_user.save +
    70. +
      + +
      +
    71. + + + + + + UserMailer.admin_approved(@the_user).deliver_now +
    72. +
      + +
      +
    73. + + + + + + redirect_to admin_users_url, notice: 'User was successfully approved.' +
    74. +
      + +
      +
    75. + + + + + + end +
    76. +
      + +
      +
    77. + + + + + + +
    78. +
      + +
      +
    79. + + + + + + def upgrade_to_admin_account +
    80. +
      + +
      +
    81. + + + + + + @the_user = User.find(params[:user_id]) +
    82. +
      + +
      +
    83. + + + + + + @the_user.update_attribute(:is_admin, true) +
    84. +
      + +
      +
    85. + + + + + + @the_user.save +
    86. +
      + +
      +
    87. + + + + + + logger.info("#{Time.now} Account id #{@the_user.id} was upgraded to admin by #{current_user.id}") +
    88. +
      + +
      +
    89. + + + + + + redirect_to admin_users_url, notice: 'User was successfully upgraded to admin account.' +
    90. +
      + +
      +
    91. + + + + + + end +
    92. +
      + +
      +
    93. + + + + + + +
    94. +
      + +
      +
    95. + + + + + + # TODO: refactor later +
    96. +
      + +
      +
    97. + + + + + + # DELETE /admin/users/1 +
    98. +
      + +
      +
    99. + + + + + + # DELETE /admin/users/1?course_id=1 +
    100. +
      + +
      +
    101. + + + + + + def destroy +
    102. +
      + +
      +
    103. + + + + + + @the_user = User.find(params[:id]) +
    104. +
      + +
      +
    105. + + + + + + +
    106. +
      + +
      +
    107. + + + + + + # Check if we are destroying the membeship, not the user +
    108. +
      + +
      +
    109. + + + + + + if params[:course_id] +
    110. +
      + +
      +
    111. + + + + + + course = Course.find(params[:course_id]) +
    112. +
      + +
      +
    113. + + + + + + +
    114. +
      + +
      +
    115. + + + + + + # see whether the user to be deleted is a guest user +
    116. +
      + +
      +
    117. + + + + + + membership = course.membership_for_user(@the_user) +
    118. +
      + +
      +
    119. + + + + + + guest = course.guest_user_finder(@the_user) +
    120. +
      + +
      +
    121. + + + + + + +
    122. +
      + +
      +
    123. + + + + + + # Redirect current user back to the course users page +
    124. +
      + +
      +
    125. + + + + + + url = course_users_url(course) +
    126. +
      + +
      +
    127. + + + + + + +
    128. +
      + +
      +
    129. + + + + + + if membership && membership.role == UserCourseMembership::ROLE_GUEST && guest.destroy && membership.destroy +
    130. +
      + +
      +
    131. + + + + + + redirect_to url, notice: "User was removed from #{course.code}." +
    132. +
      + +
      +
    133. + + + + + + elsif membership && membership.role != UserCourseMembership::ROLE_STUDENT && membership.destroy +
    134. +
      + +
      +
    135. + + + + + + redirect_to url, notice: "User was removed from #{course.code}." +
    136. +
      + +
      +
    137. + + + + + + else +
    138. +
      + +
      +
    139. + + + + + + redirect_to url, alert: "Error removing user" +
    140. +
      + +
      +
    141. + + + + + + end +
    142. +
      + +
      +
    143. + + + + + + else +
    144. +
      + +
      +
    145. + + + + + + if @the_user.is_admin +
    146. +
      + +
      +
    147. + + + + + + if @the_user.destroy +
    148. +
      + +
      +
    149. + + + + + + redirect_to admin_users_url, notice: 'User was successfully deleted.' +
    150. +
      + +
      +
    151. + + + + + + else +
    152. +
      + +
      +
    153. + + + + + + redirect_to admin_users_url, alert: @the_user.errors.to_a.join(", ") +
    154. +
      + +
      +
    155. + + + + + + end +
    156. +
      + +
      +
    157. + + + + + + else +
    158. +
      + +
      +
    159. + + + + + + if @the_user.destroy +
    160. +
      + +
      +
    161. + + + + + + redirect_to admin_users_url, notice: 'User was successfully deleted.' +
    162. +
      + +
      +
    163. + + + + + + else +
    164. +
      + +
      +
    165. + + + + + + redirect_to admin_users_url, alert: @the_user.errors.to_a.join(", ") +
    166. +
      + +
      +
    167. + + + + + + end +
    168. +
      + +
      +
    169. + + + + + + end +
    170. +
      + +
      +
    171. + + + + + + end +
    172. +
      + +
      +
    173. + + + + + + end +
    174. +
      + +
      +
    175. + + + + + + +
    176. +
      + +
      +
    177. + + + + + + protected +
    178. +
      + +
      +
    179. + + + + + + +
    180. +
      + +
      +
    181. + + + + + + def is_admin +
    182. +
      + +
      +
    183. + + + + + + unless current_user.is_admin +
    184. +
      + +
      +
    185. + + + + + + redirect_to root_url, status: 401 +
    186. +
      + +
      +
    187. + + + + + + end +
    188. +
      + +
      +
    189. + + + + + + end +
    190. +
      + +
      +
    191. + + + + + + end +
    192. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/announcements_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 94 relevant lines. + 0 lines covered and + 94 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class AnnouncementsController < ApplicationController +
    36. +
      + +
      +
    37. + + + + + + before_action { |controller| +
    38. +
      + +
      +
    39. + + + + + + if params[:announcement] and params[:announcement]["course_id"] +
    40. +
      + +
      +
    41. + + + + + + @course = Course.find(params[:announcement]["course_id"]) +
    42. +
      + +
      +
    43. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_TEACHING_ASSISTANT, +
    44. +
      + +
      +
    45. + + + + + + course: @course, +
    46. +
      + +
      +
    47. + + + + + + only: [ :index ] +
    48. +
      + +
      +
    49. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, +
    50. +
      + +
      +
    51. + + + + + + course: @course, +
    52. +
      + +
      +
    53. + + + + + + only: [ :index ] +
    54. +
      + +
      +
    55. + + + + + + end +
    56. +
      + +
      +
    57. + + + + + + } +
    58. +
      + +
      +
    59. + + + + + + +
    60. +
      + +
      +
    61. + + + + + + # GET /announcements +
    62. +
      + +
      +
    63. + + + + + + def index +
    64. +
      + +
      +
    65. + + + + + + @announcements = current_user.courses.collect { |c| c.announcements }.flatten +
    66. +
      + +
      +
    67. + + + + + + +
    68. +
      + +
      +
    69. + + + + + + end +
    70. +
      + +
      +
    71. + + + + + + +
    72. +
      + +
      +
    73. + + + + + + # GET /announcements/new +
    74. +
      + +
      +
    75. + + + + + + def new +
    76. +
      + +
      +
    77. + + + + + + @announcement = Announcement.new +
    78. +
      + +
      +
    79. + + + + + + @announceable_courses = current_user.courses.select { |course| +
    80. +
      + +
      +
    81. + + + + + + course.membership_for_user(current_user).role == UserCourseMembership::ROLE_TEACHING_STAFF +
    82. +
      + +
      +
    83. + + + + + + } +
    84. +
      + +
      +
    85. + + + + + + end +
    86. +
      + +
      +
    87. + + + + + + +
    88. +
      + +
      +
    89. + + + + + + # POST /announcements +
    90. +
      + +
      +
    91. + + + + + + def create +
    92. +
      + +
      +
    93. + + + + + + # Fill in parameters +
    94. +
      + +
      +
    95. + + + + + + @announcement = Announcement.new { |a| +
    96. +
      + +
      +
    97. + + + + + + a.title = params[:announcement]["title"] +
    98. +
      + +
      +
    99. + + + + + + a.html_content = params[:announcement]["html_content"] +
    100. +
      + +
      +
    101. + + + + + + } +
    102. +
      + +
      +
    103. + + + + + + +
    104. +
      + +
      +
    105. + + + + + + # Find course +
    106. +
      + +
      +
    107. + + + + + + course = nil +
    108. +
      + +
      +
    109. + + + + + + if params[:announcement] and params[:announcement]["course_id"] +
    110. +
      + +
      +
    111. + + + + + + course = Course.find(params[:announcement]["course_id"]) +
    112. +
      + +
      +
    113. + + + + + + end +
    114. +
      + +
      +
    115. + + + + + + +
    116. +
      + +
      +
    117. + + + + + + # Check permissions +
    118. +
      + +
      +
    119. + + + + + + @announceable_courses = current_user.courses.select { |course| +
    120. +
      + +
      +
    121. + + + + + + course.membership_for_user(current_user).role == UserCourseMembership::ROLE_TEACHING_STAFF +
    122. +
      + +
      +
    123. + + + + + + } +
    124. +
      + +
      +
    125. + + + + + + if course and @announceable_courses.include? course +
    126. +
      + +
      +
    127. + + + + + + @announcement.announceable = course +
    128. +
      + +
      +
    129. + + + + + + else +
    130. +
      + +
      +
    131. + + + + + + @announcement.errors.add :course_id, "must be selected from the available options" +
    132. +
      + +
      +
    133. + + + + + + end +
    134. +
      + +
      +
    135. + + + + + + +
    136. +
      + +
      +
    137. + + + + + + if @announcement.errors.empty? and @announcement.save +
    138. +
      + +
      +
    139. + + + + + + redirect_to announcements_url, notice: 'Announcement was successfully created.' +
    140. +
      + +
      +
    141. + + + + + + else +
    142. +
      + +
      +
    143. + + + + + + render action: "new" +
    144. +
      + +
      +
    145. + + + + + + end +
    146. +
      + +
      +
    147. + + + + + + end +
    148. +
      + +
      +
    149. + + + + + + +
    150. +
      + +
      +
    151. + + + + + + # GET /announcements/1/edit +
    152. +
      + +
      +
    153. + + + + + + def edit +
    154. +
      + +
      +
    155. + + + + + + @announcement = Announcement.find(params[:id]) +
    156. +
      + +
      +
    157. + + + + + + @announceable_courses = current_user.courses.select { |course| +
    158. +
      + +
      +
    159. + + + + + + course.membership_for_user(current_user).role == UserCourseMembership::ROLE_TEACHING_STAFF +
    160. +
      + +
      +
    161. + + + + + + } +
    162. +
      + +
      +
    163. + + + + + + end +
    164. +
      + +
      +
    165. + + + + + + +
    166. +
      + +
      +
    167. + + + + + + # PUT /announcements/1 +
    168. +
      + +
      +
    169. + + + + + + def update +
    170. +
      + +
      +
    171. + + + + + + # Find announcement +
    172. +
      + +
      +
    173. + + + + + + @announcement = Announcement.find(params[:id]) +
    174. +
      + +
      +
    175. + + + + + + +
    176. +
      + +
      +
    177. + + + + + + # Update +
    178. +
      + +
      +
    179. + + + + + + @announcement.title = params[:announcement]["title"] +
    180. +
      + +
      +
    181. + + + + + + @announcement.html_content = params[:announcement]["html_content"] +
    182. +
      + +
      +
    183. + + + + + + +
    184. +
      + +
      +
    185. + + + + + + # Find course +
    186. +
      + +
      +
    187. + + + + + + course = nil +
    188. +
      + +
      +
    189. + + + + + + if params[:announcement] and params[:announcement]["course_id"] +
    190. +
      + +
      +
    191. + + + + + + course = Course.find(params[:announcement]["course_id"]) +
    192. +
      + +
      +
    193. + + + + + + end +
    194. +
      + +
      +
    195. + + + + + + +
    196. +
      + +
      +
    197. + + + + + + # Check permissions +
    198. +
      + +
      +
    199. + + + + + + @announceable_courses = current_user.courses.select { |course| +
    200. +
      + +
      +
    201. + + + + + + course.membership_for_user(current_user).role == UserCourseMembership::ROLE_TEACHING_STAFF +
    202. +
      + +
      +
    203. + + + + + + } +
    204. +
      + +
      +
    205. + + + + + + if course and @announceable_courses.include? course +
    206. +
      + +
      +
    207. + + + + + + @announcement.announceable = course +
    208. +
      + +
      +
    209. + + + + + + else +
    210. +
      + +
      +
    211. + + + + + + @announcement.errors.add :course_id, "must be selected from the available options" +
    212. +
      + +
      +
    213. + + + + + + end +
    214. +
      + +
      +
    215. + + + + + + +
    216. +
      + +
      +
    217. + + + + + + # Check content +
    218. +
      + +
      +
    219. + + + + + + if @announcement.html_content.nil? or @announcement.html_content.empty? +
    220. +
      + +
      +
    221. + + + + + + @announcement.errors.add :html_content, "must be not blank" +
    222. +
      + +
      +
    223. + + + + + + end +
    224. +
      + +
      +
    225. + + + + + + +
    226. +
      + +
      +
    227. + + + + + + if @announcement.errors.empty? and @announcement.save +
    228. +
      + +
      +
    229. + + + + + + redirect_to announcements_url, notice: 'Announcement was successfully updated.' +
    230. +
      + +
      +
    231. + + + + + + else +
    232. +
      + +
      +
    233. + + + + + + render action: "edit" +
    234. +
      + +
      +
    235. + + + + + + end +
    236. +
      + +
      +
    237. + + + + + + end +
    238. +
      + +
      +
    239. + + + + + + +
    240. +
      + +
      +
    241. + + + + + + # DELETE /announcements/1 +
    242. +
      + +
      +
    243. + + + + + + def destroy +
    244. +
      + +
      +
    245. + + + + + + @announcement = Announcement.find(params[:id]) +
    246. +
      + +
      +
    247. + + + + + + @announcement.destroy +
    248. +
      + +
      +
    249. + + + + + + +
    250. +
      + +
      +
    251. + + + + + + redirect_to announcements_url, notice: 'Announcement was successfully deleted.' +
    252. +
      + +
      +
    253. + + + + + + end +
    254. +
      + +
      +
    255. + + + + + + end +
    256. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/application_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 77 relevant lines. + 0 lines covered and + 77 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class ApplicationController < ActionController::Base +
    36. +
      + +
      +
    37. + + + + + + protect_from_forgery +
    38. +
      + +
      +
    39. + + + + + + before_action :authenticate_user! +
    40. +
      + +
      +
    41. + + + + + + before_action :sanitize_id, only: [:index, :show, :edit, :update, :destroy] +
    42. +
      + +
      +
    43. + + + + + + +
    44. +
      + +
      +
    45. + + + + + + private +
    46. +
      + +
      +
    47. + + + + + + +
    48. +
      + +
      +
    49. + + + + + + def sanitize_id +
    50. +
      + +
      +
    51. + + + + + + begin +
    52. +
      + +
      +
    53. + + + + + + # Since we have nested controllers, we need to check for every "_id" params +
    54. +
      + +
      +
    55. + + + + + + params.each { |key, value| +
    56. +
      + +
      +
    57. + + + + + + next unless key.to_s.match(/_id$/) +
    58. +
      + +
      +
    59. + + + + + + controller_for_key = key.to_s.scan(/^(.+)_id$/).first.first +
    60. +
      + +
      +
    61. + + + + + + @obj = controller_for_key.classify.constantize.find_by_id(value) +
    62. +
      + +
      +
    63. + + + + + + unless @obj +
    64. +
      + +
      +
    65. + + + + + + message = "Could not find any #{controller_for_key.classify.tableize.humanize.pluralize.titleize} with id #{value}" +
    66. +
      + +
      +
    67. + + + + + + if action_name != "index" +
    68. +
      + +
      +
    69. + + + + + + redirect_to({ controller: controller_name, action: "index" }, alert: message) +
    70. +
      + +
      +
    71. + + + + + + else +
    72. +
      + +
      +
    73. + + + + + + redirect_to({ controller: "announcements", action: "index" }, alert: message) +
    74. +
      + +
      +
    75. + + + + + + end +
    76. +
      + +
      +
    77. + + + + + + end +
    78. +
      + +
      +
    79. + + + + + + } +
    80. +
      + +
      +
    81. + + + + + + rescue +
    82. +
      + +
      +
    83. + + + + + + # controller_name did not correspond to any known models +
    84. +
      + +
      +
    85. + + + + + + end +
    86. +
      + +
      +
    87. + + + + + + end +
    88. +
      + +
      +
    89. + + + + + + +
    90. +
      + +
      +
    91. + + + + + + # This method should be called by each controller for every role in UserCourseMembership to whitelist +
    92. +
      + +
      +
    93. + + + + + + # actions allowed for each role. The method delegates how to find the course object to the controller +
    94. +
      + +
      +
    95. + + + + + + # Example usage: +
    96. +
      + +
      +
    97. + + + + + + # +
    98. +
      + +
      +
    99. + + + + + + # before_action { |controller| +
    100. +
      + +
      +
    101. + + + + + + # @course = get_course_from_params +
    102. +
      + +
      +
    103. + + + + + + # if params[:course_id] +
    104. +
      + +
      +
    105. + + + + + + # controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_TEACHING_STAFF, +
    106. +
      + +
      +
    107. + + + + + + # course: @course, +
    108. +
      + +
      +
    109. + + + + + + # only: [ :edit ] +
    110. +
      + +
      +
    111. + + + + + + # end +
    112. +
      + +
      +
    113. + + + + + + # } +
    114. +
      + +
      +
    115. + + + + + + +
    116. +
      + +
      +
    117. + + + + + + def authenticate_actions_for_role(role, opts={}) +
    118. +
      + +
      +
    119. + + + + + + return if current_user.is_admin +
    120. +
      + +
      +
    121. + + + + + + +
    122. +
      + +
      +
    123. + + + + + + # Sanitize +
    124. +
      + +
      +
    125. + + + + + + raise unless opts[:course] +
    126. +
      + +
      +
    127. + + + + + + raise unless opts[:only] +
    128. +
      + +
      +
    129. + + + + + + +
    130. +
      + +
      +
    131. + + + + + + # Get course +
    132. +
      + +
      +
    133. + + + + + + course = opts[:course] +
    134. +
      + +
      +
    135. + + + + + + +
    136. +
      + +
      +
    137. + + + + + + # Get current user's role +
    138. +
      + +
      +
    139. + + + + + + membership = course.membership_for_user(current_user) +
    140. +
      + +
      +
    141. + + + + + + +
    142. +
      + +
      +
    143. + + + + + + # Check if we need to authenticate +
    144. +
      + +
      +
    145. + + + + + + if membership.nil? or (membership and membership.role == role and !opts[:only].include? action_name.intern) +
    146. +
      + +
      +
    147. + + + + + + redirect_to( { controller: "announcements", action: "index" }, alert: "You do not have access to the url \"#{request.env['REQUEST_URI']}\". Please contact the administrator for more information.") +
    148. +
      + +
      +
    149. + + + + + + return +
    150. +
      + +
      +
    151. + + + + + + end +
    152. +
      + +
      +
    153. + + + + + + end +
    154. +
      + +
      +
    155. + + + + + + +
    156. +
      + +
      +
    157. + + + + + + def authenticate_custom_actions_for_teaching_staff(opts) +
    158. +
      + +
      +
    159. + + + + + + return if current_user.is_admin +
    160. +
      + +
      +
    161. + + + + + + +
    162. +
      + +
      +
    163. + + + + + + # Sanitize +
    164. +
      + +
      +
    165. + + + + + + raise unless opts[:only] +
    166. +
      + +
      +
    167. + + + + + + +
    168. +
      + +
      +
    169. + + + + + + unless current_user.is_staff_or_ta? and opts[:only].include? action_name.intern +
    170. +
      + +
      +
    171. + + + + + + redirect_to( { controller: "announcements", action: "index" }, alert: "You do not have access to the url \"#{request.env['REQUEST_URI']}\". Please contact the administrator for more information.") +
    172. +
      + +
      +
    173. + + + + + + return +
    174. +
      + +
      +
    175. + + + + + + end +
    176. +
      + +
      +
    177. + + + + + + end +
    178. +
      + +
      +
    179. + + + + + + +
    180. +
      + +
      +
    181. + + + + + + def authenticate_actions_for_admin(opts) +
    182. +
      + +
      +
    183. + + + + + + raise unless opts[:only] +
    184. +
      + +
      +
    185. + + + + + + +
    186. +
      + +
      +
    187. + + + + + + # Check if we need to authenticate +
    188. +
      + +
      +
    189. + + + + + + unless current_user.is_admin +
    190. +
      + +
      +
    191. + + + + + + if opts[:only].include? action_name.intern +
    192. +
      + +
      +
    193. + + + + + + redirect_to( { controller: "announcements", action: "index" }, alert: "You do not have access to the url \"#{request.env['REQUEST_URI']}\". Please contact the administrator for more information.") +
    194. +
      + +
      +
    195. + + + + + + end +
    196. +
      + +
      +
    197. + + + + + + end +
    198. +
      + +
      +
    199. + + + + + + end +
    200. +
      + +
      +
    201. + + + + + + +
    202. +
      + +
      +
    203. + + + + + + def authenticate_actions_for_admin_or_teaching_staff(opts) +
    204. +
      + +
      +
    205. + + + + + + # Sanitize +
    206. +
      + +
      +
    207. + + + + + + raise unless opts[:only] +
    208. +
      + +
      +
    209. + + + + + + +
    210. +
      + +
      +
    211. + + + + + + user_current_role = nil +
    212. +
      + +
      +
    213. + + + + + + +
    214. +
      + +
      +
    215. + + + + + + # Get course +
    216. +
      + +
      +
    217. + + + + + + if (opts[:course] != nil && !current_user.is_admin) +
    218. +
      + +
      +
    219. + + + + + + course = opts[:course] +
    220. +
      + +
      +
    221. + + + + + + # check current user role (if any) +
    222. +
      + +
      +
    223. + + + + + + user_current_role = course.membership_for_user(current_user).role +
    224. +
      + +
      +
    225. + + + + + + end +
    226. +
      + +
      +
    227. + + + + + + +
    228. +
      + +
      +
    229. + + + + + + # Check if we need to authenticate +
    230. +
      + +
      +
    231. + + + + + + unless current_user.is_admin or user_current_role == UserCourseMembership::ROLE_TEACHING_STAFF +
    232. +
      + +
      +
    233. + + + + + + if opts[:only].include? action_name.intern +
    234. +
      + +
      +
    235. + + + + + + redirect_to( { controller: "announcements", action: "index" }, alert: "You do not have access to the url \"#{request.env['REQUEST_URI']}\". Please contact the administrator for more information.") +
    236. +
      + +
      +
    237. + + + + + + end +
    238. +
      + +
      +
    239. + + + + + + end +
    240. +
      + +
      +
    241. + + + + + + end +
    242. +
      + +
      +
    243. + + + + + + end +
    244. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/assignments_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 190 relevant lines. + 0 lines covered and + 190 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + require 'zip' +
    36. +
      + +
      +
    37. + + + + + + +
    38. +
      + +
      +
    39. + + + + + + class AssignmentsController < ApplicationController +
    40. +
      + +
      +
    41. + + + + + + +
    42. +
      + +
      +
    43. + + + + + + # Define valid zip mime types as constant variables +
    44. +
      + +
      +
    45. + + + + + + X_ZIP_COMPRESSED_MIME_TYPE = "application/x-zip-compressed" +
    46. +
      + +
      +
    47. + + + + + + ZIP_COMPRESSED_MIME_TYPE = "application/zip-compressed" +
    48. +
      + +
      +
    49. + + + + + + APPLICATION_ZIP_MIME_TYPE = "application/zip" +
    50. +
      + +
      +
    51. + + + + + + MULTIPART_X_ZIP_MIME_TYPE = "multipart/x-zip" +
    52. +
      + +
      +
    53. + + + + + + OCTET_STREAM_MIME_TYPE = "application/octet-stream" +
    54. +
      + +
      +
    55. + + + + + + +
    56. +
      + +
      +
    57. + + + + + + before_action { |controller| +
    58. +
      + +
      +
    59. + + + + + + if params[:course_id] +
    60. +
      + +
      +
    61. + + + + + + @course = Course.find(params[:course_id]) +
    62. +
      + +
      +
    63. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_TEACHING_ASSISTANT, +
    64. +
      + +
      +
    65. + + + + + + course: @course, +
    66. +
      + +
      +
    67. + + + + + + only: [ :index, :cluster_students, :new, :create, :show, :update] +
    68. +
      + +
      +
    69. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, +
    70. +
      + +
      +
    71. + + + + + + course: @course, +
    72. +
      + +
      +
    73. + + + + + + only: [ ] +
    74. +
      + +
      +
    75. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_GUEST, +
    76. +
      + +
      +
    77. + + + + + + course: @course, +
    78. +
      + +
      +
    79. + + + + + + only: [ :index ] +
    80. +
      + +
      +
    81. + + + + + + end +
    82. +
      + +
      +
    83. + + + + + + } +
    84. +
      + +
      +
    85. + + + + + + +
    86. +
      + +
      +
    87. + + + + + + # GET /courses/1/assignments +
    88. +
      + +
      +
    89. + + + + + + def index +
    90. +
      + +
      +
    91. + + + + + + # check whether is it a guest user +
    92. +
      + +
      +
    93. + + + + + + @guest_details = @course.find_guest_user_details(session[:user_id]) +
    94. +
      + +
      +
    95. + + + + + + # obtain assignment to be shown if is a guest user +
    96. +
      + +
      +
    97. + + + + + + if @guest_details.length > 0 +
    98. +
      + +
      +
    99. + + + + + + @assignment_to_be_shown = @guest_details.map{|detail| detail.assignment_id } +
    100. +
      + +
      +
    101. + + + + + + end +
    102. +
      + +
      +
    103. + + + + + + +
    104. +
      + +
      +
    105. + + + + + + @assignments = @course.assignments +
    106. +
      + +
      +
    107. + + + + + + @empty_assignments = @course.empty_assignments +
    108. +
      + +
      +
    109. + + + + + + @processing_assignments = @course.processing_assignments +
    110. +
      + +
      +
    111. + + + + + + @processed_assignments = @course.processed_assignments +
    112. +
      + +
      +
    113. + + + + + + @erroneous_assignments = @course.erroneous_assignments +
    114. +
      + +
      +
    115. + + + + + + end +
    116. +
      + +
      +
    117. + + + + + + +
    118. +
      + +
      +
    119. + + + + + + # GET /assignments/1/cluster_students +
    120. +
      + +
      +
    121. + + + + + + def cluster_students +
    122. +
      + +
      +
    123. + + + + + + @assignment = Assignment.find(params["assignment_id"]) +
    124. +
      + +
      +
    125. + + + + + + respond_to do |format| +
    126. +
      + +
      +
    127. + + + + + + format.json { +
    128. +
      + +
      +
    129. + + + + + + render json: @assignment.cluster_students.collect { |s| +
    130. +
      + +
      +
    131. + + + + + + { id: s.id, name: s.name } +
    132. +
      + +
      +
    133. + + + + + + } +
    134. +
      + +
      +
    135. + + + + + + } +
    136. +
      + +
      +
    137. + + + + + + end +
    138. +
      + +
      +
    139. + + + + + + end +
    140. +
      + +
      +
    141. + + + + + + +
    142. +
      + +
      +
    143. + + + + + + # GET /courses/1/assignments/1/log +
    144. +
      + +
      +
    145. + + + + + + def show_log +
    146. +
      + +
      +
    147. + + + + + + @assignment = Assignment.find(params[:assignment_id]) +
    148. +
      + +
      +
    149. + + + + + + end +
    150. +
      + +
      +
    151. + + + + + + +
    152. +
      + +
      +
    153. + + + + + + # GET /courses/1/assignments/1 +
    154. +
      + +
      +
    155. + + + + + + def show +
    156. +
      + +
      +
    157. + + + + + + @assignment = Assignment.find(params[:id]) +
    158. +
      + +
      +
    159. + + + + + + end +
    160. +
      + +
      +
    161. + + + + + + +
    162. +
      + +
      +
    163. + + + + + + # GET /courses/1/assignments/new +
    164. +
      + +
      +
    165. + + + + + + def new +
    166. +
      + +
      +
    167. + + + + + + @assignment = Assignment.new +
    168. +
      + +
      +
    169. + + + + + + end +
    170. +
      + +
      +
    171. + + + + + + +
    172. +
      + +
      +
    173. + + + + + + # POST /courses/1/assignments +
    174. +
      + +
      +
    175. + + + + + + def create +
    176. +
      + +
      +
    177. + + + + + + @assignment = Assignment.new { |a| +
    178. +
      + +
      +
    179. + + + + + + a.title = params[:assignment]["title"] +
    180. +
      + +
      +
    181. + + + + + + a.language = params[:assignment]["language"] +
    182. +
      + +
      +
    183. + + + + + + a.min_match_length = params[:assignment]["min_match_length"] +
    184. +
      + +
      +
    185. + + + + + + a.ngram_size = params[:assignment]["ngram_size"] +
    186. +
      + +
      +
    187. + + + + + + a.course_id = @course.id +
    188. +
      + +
      +
    189. + + + + + + } +
    190. +
      + +
      +
    191. + + + + + + +
    192. +
      + +
      +
    193. + + + + + + puts "DEBUG 06: Enable fingerprints checkbox?" +
    194. +
      + +
      +
    195. + + + + + + puts "Checkbox: #{params[:assignment]["used_fingerprints"]}" +
    196. +
      + +
      +
    197. + + + + + + # Process file if @assignment is valid and file was uploaded +
    198. +
      + +
      +
    199. + + + + + + if @assignment.valid? +
    200. +
      + +
      +
    201. + + + + + + +
    202. +
      + +
      +
    203. + + + + + + # Save assignment to obtain id +
    204. +
      + +
      +
    205. + + + + + + return render action: "new" unless @assignment.save +
    206. +
      + +
      +
    207. + + + + + + +
    208. +
      + +
      +
    209. + + + + + + isMapEnabled = (params[:assignment]["mapbox"] == "Yes")? true : false; +
    210. +
      + +
      +
    211. + + + + + + used_fingerprints = (params[:assignment]["used_fingerprints"] == "Yes")? true : false +
    212. +
      + +
      +
    213. + + + + + + +
    214. +
      + +
      +
    215. + + + + + + # No student submission file was uploaded +
    216. +
      + +
      +
    217. + + + + + + if params[:assignment]["file"].nil? +
    218. +
      + +
      +
    219. + + + + + + # Create asssignment but don't process it +
    220. +
      + +
      +
    221. + + + + + + redirect_to course_assignments_url(@course), notice: 'Assignment was successfully created. Please upload the student submission files and mapping file (if any) to process the assignment' +
    222. +
      + +
      +
    223. + + + + + + # Student submission file is a valid zip +
    224. +
      + +
      +
    225. + + + + + + elsif (is_valid_zip?(params[:assignment]["file"].content_type, params[:assignment]["file"].path)) +
    226. +
      + +
      +
    227. + + + + + + # Don't process the file and show error if the mapping was enabled but no mapping file was uploaded +
    228. +
      + +
      +
    229. + + + + + + if (is_valid_map_or_no_map?(isMapEnabled, params[:assignment]["mapfile"])) +
    230. +
      + +
      +
    231. + + + + + + self.start_upload(@assignment, params[:assignment]["file"], isMapEnabled, params[:assignment]["mapfile"], used_fingerprints) +
    232. +
      + +
      +
    233. + + + + + + # Don't process the file and show error if the mapping was enabled but no mapping file was uploaded +
    234. +
      + +
      +
    235. + + + + + + elsif (isMapEnabled && params[:assignment]["mapfile"].nil?) +
    236. +
      + +
      +
    237. + + + + + + @assignment.errors.add(:mapfile, "containing mapped student names need to be uploaded if the 'Upload map file' box is ticked") +
    238. +
      + +
      +
    239. + + + + + + return render action: "new" +
    240. +
      + +
      +
    241. + + + + + + # Don't process the file and show error if the mapping was enabled but no mapping file was uploaded +
    242. +
      + +
      +
    243. + + + + + + else +
    244. +
      + +
      +
    245. + + + + + + @assignment.errors.add :mapfile, "containing mapped student names must be a valid csv file" +
    246. +
      + +
      +
    247. + + + + + + return render action: "new" +
    248. +
      + +
      +
    249. + + + + + + end +
    250. +
      + +
      +
    251. + + + + + + # Student submission file is not a valid zip file +
    252. +
      + +
      +
    253. + + + + + + else +
    254. +
      + +
      +
    255. + + + + + + @assignment.errors.add :file, "containing student submission files must be a valid zip file" +
    256. +
      + +
      +
    257. + + + + + + if (params[:assignment]["mapfile"].nil? && isMapEnabled) +
    258. +
      + +
      +
    259. + + + + + + @assignment.errors.add :mapfile, "containing mapped student names need to be uploaded if the 'Upload map file' box is ticked" +
    260. +
      + +
      +
    261. + + + + + + elsif !(is_valid_map_or_no_map?(isMapEnabled, params[:assignment]["mapfile"])) +
    262. +
      + +
      +
    263. + + + + + + @assignment.errors.add :mapfile, "containing mapped student names must be a valid csv file" +
    264. +
      + +
      +
    265. + + + + + + end +
    266. +
      + +
      +
    267. + + + + + + return render action: "new" +
    268. +
      + +
      +
    269. + + + + + + end +
    270. +
      + +
      +
    271. + + + + + + else +
    272. +
      + +
      +
    273. + + + + + + render action: "new" +
    274. +
      + +
      +
    275. + + + + + + end +
    276. +
      + +
      +
    277. + + + + + + end +
    278. +
      + +
      +
    279. + + + + + + +
    280. +
      + +
      +
    281. + + + + + + # PUT /courses/1/assignments/1 +
    282. +
      + +
      +
    283. + + + + + + def update +
    284. +
      + +
      +
    285. + + + + + + @assignment = Assignment.find(params[:id]) +
    286. +
      + +
      +
    287. + + + + + + +
    288. +
      + +
      +
    289. + + + + + + isMapEnabled = (params[:assignment]["mapbox"] == "Yes")? true : false; +
    290. +
      + +
      +
    291. + + + + + + used_fingerprints = (params[:assignment]["used_fingerprints"] == "Yes")? true : false +
    292. +
      + +
      +
    293. + + + + + + +
    294. +
      + +
      +
    295. + + + + + + # No student submission file was uploaded +
    296. +
      + +
      +
    297. + + + + + + if params[:assignment]["file"].nil? +
    298. +
      + +
      +
    299. + + + + + + @assignment.errors.add :file, "containing student submission files need to be uploaded to process the assignment" +
    300. +
      + +
      +
    301. + + + + + + if (params[:assignment]["mapfile"].nil? && isMapEnabled) +
    302. +
      + +
      +
    303. + + + + + + @assignment.errors.add :mapfile, "containing mapped student names need to be uploaded if the 'Upload map file' box is ticked" +
    304. +
      + +
      +
    305. + + + + + + elsif !(is_valid_map_or_no_map?(isMapEnabled, params[:assignment]["mapfile"])) +
    306. +
      + +
      +
    307. + + + + + + @assignment.errors.add :mapfile, "containing mapped student names must be a valid csv file" +
    308. +
      + +
      +
    309. + + + + + + end +
    310. +
      + +
      +
    311. + + + + + + return render action: "show" +
    312. +
      + +
      +
    313. + + + + + + elsif (is_valid_zip?(params[:assignment]["file"].content_type, params[:assignment]["file"].path)) +
    314. +
      + +
      +
    315. + + + + + + # Don't process the file and show error if the mapping was enabled but no mapping file was uploaded +
    316. +
      + +
      +
    317. + + + + + + if (is_valid_map_or_no_map?(isMapEnabled, params[:assignment]["mapfile"])) +
    318. +
      + +
      +
    319. + + + + + + self.start_upload(@assignment, params[:assignment]["file"], isMapEnabled, params[:assignment]["mapfile"], used_fingerprints) +
    320. +
      + +
      +
    321. + + + + + + # Don't process the file and show error if the mapping was enabled but no mapping file was uploaded +
    322. +
      + +
      +
    323. + + + + + + elsif (isMapEnabled && params[:assignment]["mapfile"].nil?) +
    324. +
      + +
      +
    325. + + + + + + @assignment.errors.add(:mapfile, "containing mapped student names need to be uploaded if the 'Upload map file' box is ticked") +
    326. +
      + +
      +
    327. + + + + + + return render action: "show" +
    328. +
      + +
      +
    329. + + + + + + # Don't process the file and show error if the mapping was enabled but no mapping file was uploaded +
    330. +
      + +
      +
    331. + + + + + + else +
    332. +
      + +
      +
    333. + + + + + + @assignment.errors.add :mapfile, "containing mapped student names must be a valid csv file" +
    334. +
      + +
      +
    335. + + + + + + return render action: "show" +
    336. +
      + +
      +
    337. + + + + + + end +
    338. +
      + +
      +
    339. + + + + + + # Student submission file is not a valid zip file +
    340. +
      + +
      +
    341. + + + + + + else +
    342. +
      + +
      +
    343. + + + + + + @assignment.errors.add :file, "containing student submission files must be a valid zip file" +
    344. +
      + +
      +
    345. + + + + + + if (params[:assignment]["mapfile"].nil? && isMapEnabled) +
    346. +
      + +
      +
    347. + + + + + + @assignment.errors.add :mapfile, "containing mapped student names need to be uploaded if the 'Upload map file' box is ticked" +
    348. +
      + +
      +
    349. + + + + + + elsif !(is_valid_map_or_no_map?(isMapEnabled, params[:assignment]["mapfile"])) +
    350. +
      + +
      +
    351. + + + + + + @assignment.errors.add :mapfile, "containing mapped student names must be a valid csv file" +
    352. +
      + +
      +
    353. + + + + + + end +
    354. +
      + +
      +
    355. + + + + + + return render action: "show" +
    356. +
      + +
      +
    357. + + + + + + end +
    358. +
      + +
      +
    359. + + + + + + end +
    360. +
      + +
      +
    361. + + + + + + +
    362. +
      + +
      +
    363. + + + + + + # DELETE /courses/1/assignments/1 +
    364. +
      + +
      +
    365. + + + + + + def destroy +
    366. +
      + +
      +
    367. + + + + + + @assignment = Assignment.find(params[:id]) +
    368. +
      + +
      +
    369. + + + + + + @assignment.destroy +
    370. +
      + +
      +
    371. + + + + + + +
    372. +
      + +
      +
    373. + + + + + + redirect_to course_assignments_url(@course), notice: 'Assignment was successfully deleted.' +
    374. +
      + +
      +
    375. + + + + + + end +
    376. +
      + +
      +
    377. + + + + + + +
    378. +
      + +
      +
    379. + + + + + + def start_upload(assignment, submissionFile, isMapEnabled, mapFile, used_fingerprints) +
    380. +
      + +
      +
    381. + + + + + + require 'submissions_handler' +
    382. +
      + +
      +
    383. + + + + + + +
    384. +
      + +
      +
    385. + + + + + + # Process upload file +
    386. +
      + +
      +
    387. + + + + + + submissions_path = SubmissionsHandler.process_upload(submissionFile, isMapEnabled, mapFile, assignment) +
    388. +
      + +
      +
    389. + + + + + + if submissions_path +
    390. +
      + +
      +
    391. + + + + + + # Launch java program to process submissions +
    392. +
      + +
      +
    393. + + + + + + SubmissionsHandler.process_submissions(submissions_path, assignment, isMapEnabled, used_fingerprints) +
    394. +
      + +
      +
    395. + + + + + + +
    396. +
      + +
      +
    397. + + + + + + process = assignment.submission_similarity_process +
    398. +
      + +
      +
    399. + + + + + + notice = 'SSID will start to process the assignment now. Please refresh this page after a few minutes to view the similarity results.' +
    400. +
      + +
      +
    401. + + + + + + if process && process.status == SubmissionSimilarityProcess::STATUS_WAITING +
    402. +
      + +
      +
    403. + + + + + + notice = 'Your assignment has been put into a waiting list. SSID will process it soon. Thank you for your patience.' +
    404. +
      + +
      +
    405. + + + + + + end +
    406. +
      + +
      +
    407. + + + + + + redirect_to course_assignments_url(@course), notice: notice +
    408. +
      + +
      +
    409. + + + + + + else +
    410. +
      + +
      +
    411. + + + + + + assignment.errors.add "Submission zip file", ": SSID supports both directory-based and file-based submissions. Please select the submissions you want to evaluate and compress." +
    412. +
      + +
      +
    413. + + + + + + return render action: "show" +
    414. +
      + +
      +
    415. + + + + + + end +
    416. +
      + +
      +
    417. + + + + + + +
    418. +
      + +
      +
    419. + + + + + + end +
    420. +
      + +
      +
    421. + + + + + + +
    422. +
      + +
      +
    423. + + + + + + # Responsible for verifying whether a uploaded file is zip by checking its mime type and/or whether can it be extracted by the zip library. +
    424. +
      + +
      +
    425. + + + + + + # For files with mime type = application/octet-stream, it needs to be further verified by the zip library as it can be a rar file. +
    426. +
      + +
      +
    427. + + + + + + # Params: +
    428. +
      + +
      +
    429. + + + + + + # +mimeType+:: string that contains the file's mimetype +
    430. +
      + +
      +
    431. + + + + + + # +filePath+:: string that contains the file's path which is to be used by the zip library when extracting the file +
    432. +
      + +
      +
    433. + + + + + + def is_valid_zip?(mimeType, filePath) +
    434. +
      + +
      +
    435. + + + + + + # Valid zip file mime types that does not required to be further verified by the zip library +
    436. +
      + +
      +
    437. + + + + + + if mimeType == X_ZIP_COMPRESSED_MIME_TYPE || +
    438. +
      + +
      +
    439. + + + + + + mimeType == ZIP_COMPRESSED_MIME_TYPE || +
    440. +
      + +
      +
    441. + + + + + + mimeType == APPLICATION_ZIP_MIME_TYPE || +
    442. +
      + +
      +
    443. + + + + + + mimeType == MULTIPART_X_ZIP_MIME_TYPE +
    444. +
      + +
      +
    445. + + + + + + return true; +
    446. +
      + +
      +
    447. + + + + + + # Need to be further verified by zip library as it can be a rar file +
    448. +
      + +
      +
    449. + + + + + + elsif mimeType == OCTET_STREAM_MIME_TYPE && is_opened_as_zip?(filePath) +
    450. +
      + +
      +
    451. + + + + + + return true; +
    452. +
      + +
      +
    453. + + + + + + # For other mime types, safe to consider that it is not a zip file +
    454. +
      + +
      +
    455. + + + + + + return false; +
    456. +
      + +
      +
    457. + + + + + + end +
    458. +
      + +
      +
    459. + + + + + + end +
    460. +
      + +
      +
    461. + + + + + + +
    462. +
      + +
      +
    463. + + + + + + +
    464. +
      + +
      +
    465. + + + + + + # Responsible for verifying whether a uploaded file is zip by checking whether can it be extracted by the zip library +
    466. +
      + +
      +
    467. + + + + + + # Params: +
    468. +
      + +
      +
    469. + + + + + + # +filePath+:: string that contains the file's path which is to be used by the zip library when extracting the file +
    470. +
      + +
      +
    471. + + + + + + def is_opened_as_zip?(path) +
    472. +
      + +
      +
    473. + + + + + + # File is zip if the zip library is able to extract the file +
    474. +
      + +
      +
    475. + + + + + + zip = Zip::File.open(path) +
    476. +
      + +
      +
    477. + + + + + + true +
    478. +
      + +
      +
    479. + + + + + + rescue => error_msg +
    480. +
      + +
      +
    481. + + + + + + puts error_msg +
    482. +
      + +
      +
    483. + + + + + + false +
    484. +
      + +
      +
    485. + + + + + + ensure +
    486. +
      + +
      +
    487. + + + + + + zip.close if zip +
    488. +
      + +
      +
    489. + + + + + + end +
    490. +
      + +
      +
    491. + + + + + + +
    492. +
      + +
      +
    493. + + + + + + def is_valid_map_or_no_map?(isMapEnabled, mapFile) +
    494. +
      + +
      +
    495. + + + + + + if (!isMapEnabled) +
    496. +
      + +
      +
    497. + + + + + + return true +
    498. +
      + +
      +
    499. + + + + + + else +
    500. +
      + +
      +
    501. + + + + + + if (mapFile.nil?) +
    502. +
      + +
      +
    503. + + + + + + return false +
    504. +
      + +
      +
    505. + + + + + + elsif mapFile.content_type == "text/csv" && mapFile.path.split('.').last.to_s.downcase == 'csv' +
    506. +
      + +
      +
    507. + + + + + + return true +
    508. +
      + +
      +
    509. + + + + + + else +
    510. +
      + +
      +
    511. + + + + + + return false +
    512. +
      + +
      +
    513. + + + + + + end +
    514. +
      + +
      +
    515. + + + + + + end +
    516. +
      + +
      +
    517. + + + + + + end +
    518. +
      + +
      +
    519. + + + + + + +
    520. +
      + +
      +
    521. + + + + + + end +
    522. +
      + +
      +
    523. + + + + + + +
    524. +
      + +
      +
    525. + + + + + + +
    526. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/courses_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 114 relevant lines. + 0 lines covered and + 114 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class CoursesController < ApplicationController +
    36. +
      + +
      +
    37. + + + + + + before_action { |controller| +
    38. +
      + +
      +
    39. + + + + + + controller.send :authenticate_actions_for_admin, only: [ :destroy ] +
    40. +
      + +
      +
    41. + + + + + + } +
    42. +
      + +
      +
    43. + + + + + + before_action { |controller| +
    44. +
      + +
      +
    45. + + + + + + if params[:course_id] +
    46. +
      + +
      +
    47. + + + + + + @course = Course.find(params[:course_id]) +
    48. +
      + +
      +
    49. + + + + + + elsif params[:id] +
    50. +
      + +
      +
    51. + + + + + + @course = Course.find(params[:id]) +
    52. +
      + +
      +
    53. + + + + + + end +
    54. +
      + +
      +
    55. + + + + + + if @course +
    56. +
      + +
      +
    57. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_TEACHING_STAFF, +
    58. +
      + +
      +
    59. + + + + + + course: @course, +
    60. +
      + +
      +
    61. + + + + + + only: [ :index, :cluster_students , :status ] +
    62. +
      + +
      +
    63. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_TEACHING_ASSISTANT, +
    64. +
      + +
      +
    65. + + + + + + course: @course, +
    66. +
      + +
      +
    67. + + + + + + only: [ :index, :cluster_students , :status ] +
    68. +
      + +
      +
    69. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, +
    70. +
      + +
      +
    71. + + + + + + course: @course, +
    72. +
      + +
      +
    73. + + + + + + only: [ :index ] +
    74. +
      + +
      +
    75. + + + + + + end +
    76. +
      + +
      +
    77. + + + + + + } +
    78. +
      + +
      +
    79. + + + + + + +
    80. +
      + +
      +
    81. + + + + + + # GET /courses/1/status +
    82. +
      + +
      +
    83. + + + + + + def status +
    84. +
      + +
      +
    85. + + + + + + @course = Course.find(params[:course_id]) +
    86. +
      + +
      +
    87. + + + + + + @empty_assignments = @course.empty_assignments +
    88. +
      + +
      +
    89. + + + + + + @processing_assignments = @course.processing_assignments +
    90. +
      + +
      +
    91. + + + + + + @processed_assignments = @course.processed_assignments +
    92. +
      + +
      +
    93. + + + + + + @erroneous_assignments = @course.erroneous_assignments +
    94. +
      + +
      +
    95. + + + + + + end +
    96. +
      + +
      +
    97. + + + + + + +
    98. +
      + +
      +
    99. + + + + + + # GET /courses +
    100. +
      + +
      +
    101. + + + + + + def index +
    102. +
      + +
      +
    103. + + + + + + @courses = current_user.is_admin ? Course.all : current_user.courses +
    104. +
      + +
      +
    105. + + + + + + end +
    106. +
      + +
      +
    107. + + + + + + +
    108. +
      + +
      +
    109. + + + + + + # GET /courses/new +
    110. +
      + +
      +
    111. + + + + + + def new +
    112. +
      + +
      +
    113. + + + + + + @course = Course.new +
    114. +
      + +
      +
    115. + + + + + + expiry_time = Time.now.getutc +
    116. +
      + +
      +
    117. + + + + + + expiry_date = expiry_time.to_date +
    118. +
      + +
      +
    119. + + + + + + @course.expiry = DateTime.new(expiry_date.year + 1, expiry_date.month, expiry_time.mday, expiry_time.hour, expiry_time.min) +
    120. +
      + +
      +
    121. + + + + + + end +
    122. +
      + +
      +
    123. + + + + + + +
    124. +
      + +
      +
    125. + + + + + + # POST /courses +
    126. +
      + +
      +
    127. + + + + + + def create +
    128. +
      + +
      +
    129. + + + + + + @course = Course.new { |c| +
    130. +
      + +
      +
    131. + + + + + + c.code = params[:course]["code"] +
    132. +
      + +
      +
    133. + + + + + + c.name = params[:course]["name"] +
    134. +
      + +
      +
    135. + + + + + + c.academic_year = params[:course]["academic_year"] +
    136. +
      + +
      +
    137. + + + + + + c.semester = params[:course]["semester"] +
    138. +
      + +
      +
    139. + + + + + + +
    140. +
      + +
      +
    141. + + + + + + expiry_time = DateTime.parse(params[:course]["expiry"]) +
    142. +
      + +
      +
    143. + + + + + + expiry_date = expiry_time.to_date +
    144. +
      + +
      +
    145. + + + + + + c.expiry = Time.zone.local_to_utc(DateTime.new(expiry_date.year, expiry_date.month, expiry_date.mday, expiry_time.hour, expiry_time.min)) +
    146. +
      + +
      +
    147. + + + + + + } +
    148. +
      + +
      +
    149. + + + + + + +
    150. +
      + +
      +
    151. + + + + + + if @course.valid? and @course.save +
    152. +
      + +
      +
    153. + + + + + + else +
    154. +
      + +
      +
    155. + + + + + + render action: "new" +
    156. +
      + +
      +
    157. + + + + + + return +
    158. +
      + +
      +
    159. + + + + + + end +
    160. +
      + +
      +
    161. + + + + + + +
    162. +
      + +
      +
    163. + + + + + + if not current_user.is_admin +
    164. +
      + +
      +
    165. + + + + + + @membership = UserCourseMembership.new { |m| +
    166. +
      + +
      +
    167. + + + + + + m.user = current_user +
    168. +
      + +
      +
    169. + + + + + + m.course = @course +
    170. +
      + +
      +
    171. + + + + + + m.role = UserCourseMembership::ROLE_TEACHING_STAFF +
    172. +
      + +
      +
    173. + + + + + + } +
    174. +
      + +
      +
    175. + + + + + + +
    176. +
      + +
      +
    177. + + + + + + if @membership.valid? and @membership.save +
    178. +
      + +
      +
    179. + + + + + + redirect_to courses_url, notice: 'Course was successfully created.' +
    180. +
      + +
      +
    181. + + + + + + else +
    182. +
      + +
      +
    183. + + + + + + render action: "new" +
    184. +
      + +
      +
    185. + + + + + + end +
    186. +
      + +
      +
    187. + + + + + + else +
    188. +
      + +
      +
    189. + + + + + + redirect_to courses_url, notice: 'Course was successfully created.' +
    190. +
      + +
      +
    191. + + + + + + end +
    192. +
      + +
      +
    193. + + + + + + end +
    194. +
      + +
      +
    195. + + + + + + +
    196. +
      + +
      +
    197. + + + + + + # GET /courses/1/edit +
    198. +
      + +
      +
    199. + + + + + + def edit +
    200. +
      + +
      +
    201. + + + + + + @course = Course.find(params[:id]) +
    202. +
      + +
      +
    203. + + + + + + end +
    204. +
      + +
      +
    205. + + + + + + +
    206. +
      + +
      +
    207. + + + + + + # PUT /courses/1 +
    208. +
      + +
      +
    209. + + + + + + def update +
    210. +
      + +
      +
    211. + + + + + + @course = Course.find(params[:id]) +
    212. +
      + +
      +
    213. + + + + + + @course.code = params[:course]["code"] +
    214. +
      + +
      +
    215. + + + + + + @course.name = params[:course]["name"] +
    216. +
      + +
      +
    217. + + + + + + @course.academic_year = params[:course]["academic_year"] +
    218. +
      + +
      +
    219. + + + + + + @course.semester = params[:course]["semester"] +
    220. +
      + +
      +
    221. + + + + + + +
    222. +
      + +
      +
    223. + + + + + + expiry_time = DateTime.parse(params[:course]["expiry"]) +
    224. +
      + +
      +
    225. + + + + + + expiry_date = expiry_time.to_date +
    226. +
      + +
      +
    227. + + + + + + @course.expiry = Time.zone.local_to_utc(DateTime.new(expiry_date.year, expiry_date.month, expiry_date.mday, expiry_time.hour, expiry_time.min)) +
    228. +
      + +
      +
    229. + + + + + + +
    230. +
      + +
      +
    231. + + + + + + if @course.errors.empty? and @course.save +
    232. +
      + +
      +
    233. + + + + + + redirect_to courses_url, notice: 'Course was successfully updated.' +
    234. +
      + +
      +
    235. + + + + + + else +
    236. +
      + +
      +
    237. + + + + + + render action: "new" +
    238. +
      + +
      +
    239. + + + + + + end +
    240. +
      + +
      +
    241. + + + + + + end +
    242. +
      + +
      +
    243. + + + + + + +
    244. +
      + +
      +
    245. + + + + + + # DELETE /courses/1 +
    246. +
      + +
      +
    247. + + + + + + def destroy +
    248. +
      + +
      +
    249. + + + + + + @course = Course.find(params[:id]) +
    250. +
      + +
      +
    251. + + + + + + @course.destroy +
    252. +
      + +
      +
    253. + + + + + + +
    254. +
      + +
      +
    255. + + + + + + redirect_to courses_url, notice: 'Course was successfully deleted.' +
    256. +
      + +
      +
    257. + + + + + + end +
    258. +
      + +
      +
    259. + + + + + + +
    260. +
      + +
      +
    261. + + + + + + # GET /courses/1/cluster_students +
    262. +
      + +
      +
    263. + + + + + + def cluster_students +
    264. +
      + +
      +
    265. + + + + + + respond_to do |format| +
    266. +
      + +
      +
    267. + + + + + + format.json { +
    268. +
      + +
      +
    269. + + + + + + render json: @course.cluster_students.collect { |s| +
    270. +
      + +
      +
    271. + + + + + + { id: s.id, name: s.name } +
    272. +
      + +
      +
    273. + + + + + + } +
    274. +
      + +
      +
    275. + + + + + + } +
    276. +
      + +
      +
    277. + + + + + + end +
    278. +
      + +
      +
    279. + + + + + + end +
    280. +
      + +
      +
    281. + + + + + + +
    282. +
      + +
      +
    283. + + + + + + end +
    284. +
      + +
      +
    285. + + + + + + +
    286. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/submission_cluster_groups_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 93 relevant lines. + 0 lines covered and + 93 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionClusterGroupsController < ApplicationController +
    36. +
      + +
      +
    37. + + + + + + before_action { |controller| +
    38. +
      + +
      +
    39. + + + + + + if params[:assignment_id] +
    40. +
      + +
      +
    41. + + + + + + @assignment = Assignment.find(params[:assignment_id]) +
    42. +
      + +
      +
    43. + + + + + + @course = @assignment.course +
    44. +
      + +
      +
    45. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_TEACHING_ASSISTANT, +
    46. +
      + +
      +
    47. + + + + + + course: @course, +
    48. +
      + +
      +
    49. + + + + + + only: [ :index, :new, :create ] +
    50. +
      + +
      +
    51. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, +
    52. +
      + +
      +
    53. + + + + + + course: @course, +
    54. +
      + +
      +
    55. + + + + + + only: [ ] +
    56. +
      + +
      +
    57. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_GUEST, +
    58. +
      + +
      +
    59. + + + + + + course: @course, +
    60. +
      + +
      +
    61. + + + + + + only: [ ] +
    62. +
      + +
      +
    63. + + + + + + end +
    64. +
      + +
      +
    65. + + + + + + } +
    66. +
      + +
      +
    67. + + + + + + +
    68. +
      + +
      +
    69. + + + + + + # GET /assignments/1/cluster_groups +
    70. +
      + +
      +
    71. + + + + + + def index +
    72. +
      + +
      +
    73. + + + + + + @cluster_groups = @assignment.submission_cluster_groups +
    74. +
      + +
      +
    75. + + + + + + end +
    76. +
      + +
      +
    77. + + + + + + +
    78. +
      + +
      +
    79. + + + + + + # GET /assignments/1/cluster_groups/new +
    80. +
      + +
      +
    81. + + + + + + def new +
    82. +
      + +
      +
    83. + + + + + + @assignment_plagiarism_cases = [] +
    84. +
      + +
      +
    85. + + + + + + @assignment_plagiarism_cases[SubmissionClusterGroup::TYPE_CONFIRMED_OR_SUSPECTED_PLAGIARISM_CRITERION] = +
    86. +
      + +
      +
    87. + + + + + + @assignment.suspected_plagiarism_cases + @assignment.confirmed_plagiarism_cases +
    88. +
      + +
      +
    89. + + + + + + @assignment_plagiarism_cases[SubmissionClusterGroup::TYPE_CONFIRMED_PLAGIARISM_CRITERION] = +
    90. +
      + +
      +
    91. + + + + + + @assignment.confirmed_plagiarism_cases +
    92. +
      + +
      +
    93. + + + + + + @submission_cluster_group = SubmissionClusterGroup.new +
    94. +
      + +
      +
    95. + + + + + + end +
    96. +
      + +
      +
    97. + + + + + + +
    98. +
      + +
      +
    99. + + + + + + # POST /assignments/1/cluster_groups +
    100. +
      + +
      +
    101. + + + + + + def create +
    102. +
      + +
      +
    103. + + + + + + # Determine cut-off criterion if type requires +
    104. +
      + +
      +
    105. + + + + + + if params[:submission_cluster_group]["cut_off_criterion_type"] == SubmissionClusterGroup::TYPE_CONFIRMED_OR_SUSPECTED_PLAGIARISM_CRITERION.to_s +
    106. +
      + +
      +
    107. + + + + + + params[:submission_cluster_group]["cut_off_criterion"] = @assignment.confirmed_or_suspected_plagiarism_cases.collect { |submission_similarity| +
    108. +
      + +
      +
    109. + + + + + + submission_similarity.similarity +
    110. +
      + +
      +
    111. + + + + + + }.min +
    112. +
      + +
      +
    113. + + + + + + elsif params[:submission_cluster_group]["cut_off_criterion_type"] == SubmissionClusterGroup::TYPE_CONFIRMED_PLAGIARISM_CRITERION.to_s +
    114. +
      + +
      +
    115. + + + + + + params[:submission_cluster_group]["cut_off_criterion"] = @assignment.confirmed_plagiarism_cases.collect { |submission_similarity| +
    116. +
      + +
      +
    117. + + + + + + submission_similarity.similarity +
    118. +
      + +
      +
    119. + + + + + + }.min +
    120. +
      + +
      +
    121. + + + + + + end +
    122. +
      + +
      +
    123. + + + + + + +
    124. +
      + +
      +
    125. + + + + + + @submission_cluster_group = SubmissionClusterGroup.new { |scg| +
    126. +
      + +
      +
    127. + + + + + + scg.cut_off_criterion_type = params[:submission_cluster_group]["cut_off_criterion_type"] +
    128. +
      + +
      +
    129. + + + + + + scg.cut_off_criterion = params[:submission_cluster_group]["cut_off_criterion"] +
    130. +
      + +
      +
    131. + + + + + + scg.description = SubmissionClusterGroup::DESCRIPTIONS[params[:submission_cluster_group]["cut_off_criterion_type"].to_i] +
    132. +
      + +
      +
    133. + + + + + + scg.assignment_id = @assignment.id +
    134. +
      + +
      +
    135. + + + + + + } +
    136. +
      + +
      +
    137. + + + + + + +
    138. +
      + +
      +
    139. + + + + + + # Run java program if @submission_cluster_group is valid +
    140. +
      + +
      +
    141. + + + + + + if @submission_cluster_group.valid? +
    142. +
      + +
      +
    143. + + + + + + require 'submissions_handler' +
    144. +
      + +
      +
    145. + + + + + + +
    146. +
      + +
      +
    147. + + + + + + # Rollback if clustering error +
    148. +
      + +
      +
    149. + + + + + + error_message = "" +
    150. +
      + +
      +
    151. + + + + + + @submission_cluster_group.transaction do +
    152. +
      + +
      +
    153. + + + + + + @submission_cluster_group.save +
    154. +
      + +
      +
    155. + + + + + + begin +
    156. +
      + +
      +
    157. + + + + + + SubmissionsHandler.process_cluster_group(@submission_cluster_group) +
    158. +
      + +
      +
    159. + + + + + + redirect_to assignment_cluster_groups_url(@assignment), notice: 'Submission cluster group was successfully created.' +
    160. +
      + +
      +
    161. + + + + + + rescue Exception => e +
    162. +
      + +
      +
    163. + + + + + + error_message = e.message +
    164. +
      + +
      +
    165. + + + + + + raise ActiveRecord::Rollback +
    166. +
      + +
      +
    167. + + + + + + end +
    168. +
      + +
      +
    169. + + + + + + end +
    170. +
      + +
      +
    171. + + + + + + +
    172. +
      + +
      +
    173. + + + + + + # Render #new and display errors +
    174. +
      + +
      +
    175. + + + + + + unless @submission_cluster_group.id +
    176. +
      + +
      +
    177. + + + + + + @assignment_plagiarism_cases = [] +
    178. +
      + +
      +
    179. + + + + + + @assignment_plagiarism_cases[SubmissionClusterGroup::TYPE_CONFIRMED_OR_SUSPECTED_PLAGIARISM_CRITERION] = +
    180. +
      + +
      +
    181. + + + + + + @assignment.suspected_plagiarism_cases + @assignment.confirmed_plagiarism_cases +
    182. +
      + +
      +
    183. + + + + + + @assignment_plagiarism_cases[SubmissionClusterGroup::TYPE_CONFIRMED_PLAGIARISM_CRITERION] = +
    184. +
      + +
      +
    185. + + + + + + @assignment.confirmed_plagiarism_cases +
    186. +
      + +
      +
    187. + + + + + + @submission_cluster_group.errors.add :base, error_message +
    188. +
      + +
      +
    189. + + + + + + render action: "new" +
    190. +
      + +
      +
    191. + + + + + + end +
    192. +
      + +
      +
    193. + + + + + + else +
    194. +
      + +
      +
    195. + + + + + + @assignment_plagiarism_cases = [] +
    196. +
      + +
      +
    197. + + + + + + @assignment_plagiarism_cases[SubmissionClusterGroup::TYPE_CONFIRMED_OR_SUSPECTED_PLAGIARISM_CRITERION] = +
    198. +
      + +
      +
    199. + + + + + + @assignment.suspected_plagiarism_cases + @assignment.confirmed_plagiarism_cases +
    200. +
      + +
      +
    201. + + + + + + @assignment_plagiarism_cases[SubmissionClusterGroup::TYPE_CONFIRMED_PLAGIARISM_CRITERION] = +
    202. +
      + +
      +
    203. + + + + + + @assignment.confirmed_plagiarism_cases +
    204. +
      + +
      +
    205. + + + + + + render action: "new" +
    206. +
      + +
      +
    207. + + + + + + end +
    208. +
      + +
      +
    209. + + + + + + end +
    210. +
      + +
      +
    211. + + + + + + +
    212. +
      + +
      +
    213. + + + + + + # DELETE /assignments/1/cluster_groups/1 +
    214. +
      + +
      +
    215. + + + + + + def destroy +
    216. +
      + +
      +
    217. + + + + + + @submission_cluster_group = SubmissionClusterGroup.find(params[:id]) +
    218. +
      + +
      +
    219. + + + + + + @submission_cluster_group.destroy +
    220. +
      + +
      +
    221. + + + + + + +
    222. +
      + +
      +
    223. + + + + + + redirect_to assignment_cluster_groups_url(@assignment), notice: 'Submission cluster group was successfully deleted.' +
    224. +
      + +
      +
    225. + + + + + + end +
    226. +
      + +
      +
    227. + + + + + + end +
    228. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/submission_cluster_memberships_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 15 relevant lines. + 0 lines covered and + 15 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionClusterMembershipsController < ApplicationController +
    36. +
      + +
      +
    37. + + + + + + end +
    38. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/submission_clusters_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 121 relevant lines. + 0 lines covered and + 121 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionClustersController < ApplicationController +
    36. +
      + +
      +
    37. + + + + + + before_action { |controller| +
    38. +
      + +
      +
    39. + + + + + + @course = nil +
    40. +
      + +
      +
    41. + + + + + + if params[:id] +
    42. +
      + +
      +
    43. + + + + + + @submission_cluster = SubmissionCluster.find(params[:id]) +
    44. +
      + +
      +
    45. + + + + + + @course = @submission_cluster.course +
    46. +
      + +
      +
    47. + + + + + + end +
    48. +
      + +
      +
    49. + + + + + + if params["ids"] +
    50. +
      + +
      +
    51. + + + + + + @submissions = params["ids"].collect { |id| +
    52. +
      + +
      +
    53. + + + + + + Submission.find(id) +
    54. +
      + +
      +
    55. + + + + + + } +
    56. +
      + +
      +
    57. + + + + + + @assignment = @submissions.first.assignment +
    58. +
      + +
      +
    59. + + + + + + @course = @assignment.course +
    60. +
      + +
      +
    61. + + + + + + end +
    62. +
      + +
      +
    63. + + + + + + if params["course_id"] +
    64. +
      + +
      +
    65. + + + + + + @course = Course.find(params["course_id"]) +
    66. +
      + +
      +
    67. + + + + + + end +
    68. +
      + +
      +
    69. + + + + + + if params["assignment_id"] +
    70. +
      + +
      +
    71. + + + + + + @assignment = Assignment.find(params["assignment_id"]) +
    72. +
      + +
      +
    73. + + + + + + @course = @assignment.course +
    74. +
      + +
      +
    75. + + + + + + end +
    76. +
      + +
      +
    77. + + + + + + if params["cluster_group_id"] +
    78. +
      + +
      +
    79. + + + + + + @submission_cluster_group = SubmissionClusterGroup.find(params[:cluster_group_id]) +
    80. +
      + +
      +
    81. + + + + + + @assignment = @submission_cluster_group.assignment +
    82. +
      + +
      +
    83. + + + + + + @course = @assignment.course +
    84. +
      + +
      +
    85. + + + + + + end +
    86. +
      + +
      +
    87. + + + + + + if params["clusterIds"] +
    88. +
      + +
      +
    89. + + + + + + @clusters = params["clusterIds"].collect { |id| +
    90. +
      + +
      +
    91. + + + + + + SubmissionCluster.find(id) +
    92. +
      + +
      +
    93. + + + + + + } +
    94. +
      + +
      +
    95. + + + + + + @course = @clusters.first.course +
    96. +
      + +
      +
    97. + + + + + + end +
    98. +
      + +
      +
    99. + + + + + + if @course +
    100. +
      + +
      +
    101. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, +
    102. +
      + +
      +
    103. + + + + + + course: @course, +
    104. +
      + +
      +
    105. + + + + + + only: [ ] +
    106. +
      + +
      +
    107. + + + + + + end +
    108. +
      + +
      +
    109. + + + + + + } +
    110. +
      + +
      +
    111. + + + + + + +
    112. +
      + +
      +
    113. + + + + + + # GET /clusters/1.json +
    114. +
      + +
      +
    115. + + + + + + def show +
    116. +
      + +
      +
    117. + + + + + + @submission_cluster_id = @submission_cluster.id +
    118. +
      + +
      +
    119. + + + + + + @assignment = @submission_cluster.submission_cluster_group.assignment +
    120. +
      + +
      +
    121. + + + + + + @submissions = @submission_cluster.submissions +
    122. +
      + +
      +
    123. + + + + + + end +
    124. +
      + +
      +
    125. + + + + + + +
    126. +
      + +
      +
    127. + + + + + + # GET /clusters/show_for_submission_ids.json +
    128. +
      + +
      +
    129. + + + + + + def show_for_submission_ids +
    130. +
      + +
      +
    131. + + + + + + @submission_cluster_id = params["ids"].join("_") +
    132. +
      + +
      +
    133. + + + + + + render "show" +
    134. +
      + +
      +
    135. + + + + + + end +
    136. +
      + +
      +
    137. + + + + + + +
    138. +
      + +
      +
    139. + + + + + + # GET /clusters/ids_and_group_ids_for_student_ids.json +
    140. +
      + +
      +
    141. + + + + + + def ids_and_group_ids_for_student_ids +
    142. +
      + +
      +
    143. + + + + + + clusters = [] +
    144. +
      + +
      +
    145. + + + + + + if params["course_id"] +
    146. +
      + +
      +
    147. + + + + + + clusters = @course.submission_clusters +
    148. +
      + +
      +
    149. + + + + + + elsif params["assignment_id"] +
    150. +
      + +
      +
    151. + + + + + + clusters = @assignment.submission_clusters +
    152. +
      + +
      +
    153. + + + + + + end +
    154. +
      + +
      +
    155. + + + + + + clusters = clusters.select { |c| +
    156. +
      + +
      +
    157. + + + + + + c.submissions.any? { |s| +
    158. +
      + +
      +
    159. + + + + + + params["student_ids"].include? s.student.id.to_s +
    160. +
      + +
      +
    161. + + + + + + } +
    162. +
      + +
      +
    163. + + + + + + } +
    164. +
      + +
      +
    165. + + + + + + respond_to do |format| +
    166. +
      + +
      +
    167. + + + + + + format.json { +
    168. +
      + +
      +
    169. + + + + + + render json: clusters.collect { |c| +
    170. +
      + +
      +
    171. + + + + + + { id: c.id, group_id: c.submission_cluster_group.id } +
    172. +
      + +
      +
    173. + + + + + + } +
    174. +
      + +
      +
    175. + + + + + + } +
    176. +
      + +
      +
    177. + + + + + + end +
    178. +
      + +
      +
    179. + + + + + + end +
    180. +
      + +
      +
    181. + + + + + + +
    182. +
      + +
      +
    183. + + + + + + # GET /clusters/1/show_graph_partial +
    184. +
      + +
      +
    185. + + + + + + # GET /clusters/show_graph_partial => not working +
    186. +
      + +
      +
    187. + + + + + + def show_graph_partial +
    188. +
      + +
      +
    189. + + + + + + if params[:id] +
    190. +
      + +
      +
    191. + + + + + + render partial: "submission_cluster_graph", locals: { cluster_id: @submission_cluster.id, submission_student_ids: @submission_cluster.submission_student_ids } +
    192. +
      + +
      +
    193. + + + + + + else +
    194. +
      + +
      +
    195. + + + + + + render partial: "submission_cluster_graph" +
    196. +
      + +
      +
    197. + + + + + + end +
    198. +
      + +
      +
    199. + + + + + + end +
    200. +
      + +
      +
    201. + + + + + + +
    202. +
      + +
      +
    203. + + + + + + # GET /clusters/1/show_table_partial +
    204. +
      + +
      +
    205. + + + + + + def show_table_partial +
    206. +
      + +
      +
    207. + + + + + + render partial: "submission_cluster_table", locals: { cluster: @submission_cluster } +
    208. +
      + +
      +
    209. + + + + + + end +
    210. +
      + +
      +
    211. + + + + + + +
    212. +
      + +
      +
    213. + + + + + + # GET /clusters/show_ranking_partial +
    214. +
      + +
      +
    215. + + + + + + def show_ranking_partial +
    216. +
      + +
      +
    217. + + + + + + student_assignments = {} +
    218. +
      + +
      +
    219. + + + + + + student_clusters = {} +
    220. +
      + +
      +
    221. + + + + + + locals = {} +
    222. +
      + +
      +
    223. + + + + + + locals[:num_assignments] = {} +
    224. +
      + +
      +
    225. + + + + + + locals[:num_clusters] = {} +
    226. +
      + +
      +
    227. + + + + + + locals[:students] = @clusters.collect { |cluster| +
    228. +
      + +
      +
    229. + + + + + + cluster.submissions.collect { |submission| +
    230. +
      + +
      +
    231. + + + + + + student_assignments[submission.student.id] ||= [] +
    232. +
      + +
      +
    233. + + + + + + student_clusters[submission.student.id] ||= [] +
    234. +
      + +
      +
    235. + + + + + + unless student_assignments[submission.student.id].include? submission.assignment +
    236. +
      + +
      +
    237. + + + + + + student_assignments[submission.student.id] << submission.assignment +
    238. +
      + +
      +
    239. + + + + + + end +
    240. +
      + +
      +
    241. + + + + + + unless student_clusters[submission.student.id].include? cluster +
    242. +
      + +
      +
    243. + + + + + + student_clusters[submission.student.id] << cluster +
    244. +
      + +
      +
    245. + + + + + + end +
    246. +
      + +
      +
    247. + + + + + + submission.student +
    248. +
      + +
      +
    249. + + + + + + } +
    250. +
      + +
      +
    251. + + + + + + }.flatten.uniq.sort_by { |student| +
    252. +
      + +
      +
    253. + + + + + + 1.0 / student_clusters[student.id].size +
    254. +
      + +
      +
    255. + + + + + + } +
    256. +
      + +
      +
    257. + + + + + + student_assignments.each_key { |k| locals[:num_assignments][k] = student_assignments[k].size } +
    258. +
      + +
      +
    259. + + + + + + student_clusters.each_key { |k| locals[:num_clusters][k] = student_clusters[k].size } +
    260. +
      + +
      +
    261. + + + + + + render partial: "submission_cluster_ranking", locals: locals +
    262. +
      + +
      +
    263. + + + + + + end +
    264. +
      + +
      +
    265. + + + + + + +
    266. +
      + +
      +
    267. + + + + + + # GET /cluster_groups/1/clusters +
    268. +
      + +
      +
    269. + + + + + + def index +
    270. +
      + +
      +
    271. + + + + + + @submission_clusters = @submission_cluster_group.clusters.sort_by { |sc| +
    272. +
      + +
      +
    273. + + + + + + 1.0 / sc.submissions.size +
    274. +
      + +
      +
    275. + + + + + + } +
    276. +
      + +
      +
    277. + + + + + + end +
    278. +
      + +
      +
    279. + + + + + + end +
    280. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/submission_logs_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 51 relevant lines. + 0 lines covered and + 51 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionLogsController < ApplicationController +
    36. +
      + +
      +
    37. + + + + + + before_action { |controller| +
    38. +
      + +
      +
    39. + + + + + + if params[:assignment_id] and params[:submission_similarity_id] +
    40. +
      + +
      +
    41. + + + + + + controller.send :authenticate_custom_actions_for_teaching_staff, only: [:view_similarity] +
    42. +
      + +
      +
    43. + + + + + + elsif params[:assignment_id] +
    44. +
      + +
      +
    45. + + + + + + @assignment = Assignment.find(params["assignment_id"]) +
    46. +
      + +
      +
    47. + + + + + + @course = @assignment.course +
    48. +
      + +
      +
    49. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, +
    50. +
      + +
      +
    51. + + + + + + course: @course, +
    52. +
      + +
      +
    53. + + + + + + only: [ ] +
    54. +
      + +
      +
    55. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_GUEST, +
    56. +
      + +
      +
    57. + + + + + + course: @course, +
    58. +
      + +
      +
    59. + + + + + + only: [ ] +
    60. +
      + +
      +
    61. + + + + + + end +
    62. +
      + +
      +
    63. + + + + + + } +
    64. +
      + +
      +
    65. + + + + + + +
    66. +
      + +
      +
    67. + + + + + + # GET /assignments/1/submissions/1/log +
    68. +
      + +
      +
    69. + + + + + + def index +
    70. +
      + +
      +
    71. + + + + + + @submission = Submission.find(params["submission_id"]) +
    72. +
      + +
      +
    73. + + + + + + @submission_logs = @submission.logs +
    74. +
      + +
      +
    75. + + + + + + end +
    76. +
      + +
      +
    77. + + + + + + +
    78. +
      + +
      +
    79. + + + + + + # GET /assignments/1/submission_similarities/1/guest_user +
    80. +
      + +
      +
    81. + + + + + + def view_similarity +
    82. +
      + +
      +
    83. + + + + + + @assignment = Assignment.find(params[:assignment_id]) +
    84. +
      + +
      +
    85. + + + + + + +
    86. +
      + +
      +
    87. + + + + + + unless current_user.is_admin +
    88. +
      + +
      +
    89. + + + + + + course = @assignment.course +
    90. +
      + +
      +
    91. + + + + + + membership = course.membership_for_user(current_user) +
    92. +
      + +
      +
    93. + + + + + + has_guest_detail = GuestUsersDetail.find_by_user_id_and_assignment_id(current_user.id, @assignment.id) +
    94. +
      + +
      +
    95. + + + + + + +
    96. +
      + +
      +
    97. + + + + + + if membership.nil? +
    98. +
      + +
      +
    99. + + + + + + # Create a guest membership for current user +
    100. +
      + +
      +
    101. + + + + + + CoursesService.create_guest_membership(course, current_user) +
    102. +
      + +
      +
    103. + + + + + + CoursesService.create_guest_detail_entry(@assignment, current_user) +
    104. +
      + +
      +
    105. + + + + + + else +
    106. +
      + +
      +
    107. + + + + + + if membership.role == UserCourseMembership::ROLE_GUEST && has_guest_detail.nil? +
    108. +
      + +
      +
    109. + + + + + + CoursesService.create_guest_detail_entry(@assignment, current_user) +
    110. +
      + +
      +
    111. + + + + + + end +
    112. +
      + +
      +
    113. + + + + + + end +
    114. +
      + +
      +
    115. + + + + + + end +
    116. +
      + +
      +
    117. + + + + + + +
    118. +
      + +
      +
    119. + + + + + + submission_similarity = SubmissionSimilarity.find(params[:submission_similarity_id]) +
    120. +
      + +
      +
    121. + + + + + + redirect_to assignment_submission_similarity_path(@assignment, submission_similarity) +
    122. +
      + +
      +
    123. + + + + + + end +
    124. +
      + +
      +
    125. + + + + + + +
    126. +
      + +
      +
    127. + + + + + + end +
    128. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/submission_similarities_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 182 relevant lines. + 0 lines covered and + 182 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionSimilaritiesController < ApplicationController +
    36. +
      + +
      +
    37. + + + + + + +
    38. +
      + +
      +
    39. + + + + + + before_action { |controller| +
    40. +
      + +
      +
    41. + + + + + + @course = nil +
    42. +
      + +
      +
    43. + + + + + + if params[:assignment_id] +
    44. +
      + +
      +
    45. + + + + + + @assignment = Assignment.find(params["assignment_id"]) +
    46. +
      + +
      +
    47. + + + + + + @course = @assignment.course +
    48. +
      + +
      +
    49. + + + + + + end +
    50. +
      + +
      +
    51. + + + + + + if params[:course_id] +
    52. +
      + +
      +
    53. + + + + + + @course = Course.find(params[:course_id]) +
    54. +
      + +
      +
    55. + + + + + + end +
    56. +
      + +
      +
    57. + + + + + + if @course +
    58. +
      + +
      +
    59. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, +
    60. +
      + +
      +
    61. + + + + + + course: @course, +
    62. +
      + +
      +
    63. + + + + + + only: [ ] +
    64. +
      + +
      +
    65. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_GUEST, +
    66. +
      + +
      +
    67. + + + + + + course: @course, +
    68. +
      + +
      +
    69. + + + + + + only: [ :index, :show ] +
    70. +
      + +
      +
    71. + + + + + + end +
    72. +
      + +
      +
    73. + + + + + + } +
    74. +
      + +
      +
    75. + + + + + + +
    76. +
      + +
      +
    77. + + + + + + # GET /assignments/1/submission_similarities +
    78. +
      + +
      +
    79. + + + + + + def index +
    80. +
      + +
      +
    81. + + + + + + # obtain parameters (if any) +
    82. +
      + +
      +
    83. + + + + + + @hashed_url = session[:hashed_url] +
    84. +
      + +
      +
    85. + + + + + + @displayDialog = session[:displayDialog] +
    86. +
      + +
      +
    87. + + + + + + session[:displayDialog] = false +
    88. +
      + +
      +
    89. + + + + + + +
    90. +
      + +
      +
    91. + + + + + + @submission_similarities = SubmissionSimilarity.where( +
    92. +
      + +
      +
    93. + + + + + + assignment_id: @assignment.id +
    94. +
      + +
      +
    95. + + + + + + ).order('similarity desc').paginate(page: params[:page], per_page: 20) +
    96. +
      + +
      +
    97. + + + + + + end +
    98. +
      + +
      +
    99. + + + + + + +
    100. +
      + +
      +
    101. + + + + + + def create_guest_user +
    102. +
      + +
      +
    103. + + + + + + @assignment = Assignment.find(params[:id]) +
    104. +
      + +
      +
    105. + + + + + + @course = @assignment.course +
    106. +
      + +
      +
    107. + + + + + + +
    108. +
      + +
      +
    109. + + + + + + @user = User.find_by_id(session[:user_id]) +
    110. +
      + +
      +
    111. + + + + + + @guest_user = @course.guest_user_finder(@user) +
    112. +
      + +
      +
    113. + + + + + + # do not allow to create a sharable link if user is a guest user +
    114. +
      + +
      +
    115. + + + + + + if (@guest_user) +
    116. +
      + +
      +
    117. + + + + + + redirect_to assignment_submission_similarities_url(params[:id]), notice: 'You cannot create a sharable link as you are using SSID as a guest user. To create a sharable link and/or use other features, log in to SSID.' +
    118. +
      + +
      +
    119. + + + + + + else +
    120. +
      + +
      +
    121. + + + + + + # create a secure hash +
    122. +
      + +
      +
    123. + + + + + + hash_string = SecureRandom.urlsafe_base64(9).gsub(/-|_/,('a'..'z').to_a[rand(26)]) +
    124. +
      + +
      +
    125. + + + + + + +
    126. +
      + +
      +
    127. + + + + + + # hashed public url +
    128. +
      + +
      +
    129. + + + + + + @hashed_url = request.base_url + "/guest_user/" + hash_string +
    130. +
      + +
      +
    131. + + + + + + +
    132. +
      + +
      +
    133. + + + + + + # create the user in database +
    134. +
      + +
      +
    135. + + + + + + @guest_user = User.new { |u| +
    136. +
      + +
      +
    137. + + + + + + u.name = hash_string +
    138. +
      + +
      +
    139. + + + + + + u.full_name = hash_string +
    140. +
      + +
      +
    141. + + + + + + u.password_digest = BCrypt::Password.create("password") +
    142. +
      + +
      +
    143. + + + + + + u.email = "#{hash_string}@ssid.example.com" +
    144. +
      + +
      +
    145. + + + + + + u.is_admin_approved = 1 +
    146. +
      + +
      +
    147. + + + + + + u.activated = 1 +
    148. +
      + +
      +
    149. + + + + + + u.activated_at = Time.zone.now +
    150. +
      + +
      +
    151. + + + + + + } +
    152. +
      + +
      +
    153. + + + + + + +
    154. +
      + +
      +
    155. + + + + + + # create a entry under other tables in database +
    156. +
      + +
      +
    157. + + + + + + @guest_user.transaction do +
    158. +
      + +
      +
    159. + + + + + + if @guest_user.save and @course +
    160. +
      + +
      +
    161. + + + + + + # create entry under membership +
    162. +
      + +
      +
    163. + + + + + + membership = UserCourseMembership.new { |m| +
    164. +
      + +
      +
    165. + + + + + + m.role = 3 +
    166. +
      + +
      +
    167. + + + + + + m.user = @guest_user +
    168. +
      + +
      +
    169. + + + + + + m.course = @course +
    170. +
      + +
      +
    171. + + + + + + } +
    172. +
      + +
      +
    173. + + + + + + raise ActiveRecord::Rollback unless membership.save +
    174. +
      + +
      +
    175. + + + + + + +
    176. +
      + +
      +
    177. + + + + + + # create entry under guest user +
    178. +
      + +
      +
    179. + + + + + + guest = GuestUsersDetail.new { |m| +
    180. +
      + +
      +
    181. + + + + + + m.user_id = @guest_user.id +
    182. +
      + +
      +
    183. + + + + + + m.course_id = @course.id +
    184. +
      + +
      +
    185. + + + + + + m.hash_string = hash_string +
    186. +
      + +
      +
    187. + + + + + + m.assignment_id = @assignment.id +
    188. +
      + +
      +
    189. + + + + + + } +
    190. +
      + +
      +
    191. + + + + + + raise ActiveRecord::Rollback unless guest.save +
    192. +
      + +
      +
    193. + + + + + + +
    194. +
      + +
      +
    195. + + + + + + end +
    196. +
      + +
      +
    197. + + + + + + end +
    198. +
      + +
      +
    199. + + + + + + +
    200. +
      + +
      +
    201. + + + + + + # save the parameters and redirect +
    202. +
      + +
      +
    203. + + + + + + session[:hashed_url] = @hashed_url +
    204. +
      + +
      +
    205. + + + + + + session[:displayDialog] = true +
    206. +
      + +
      +
    207. + + + + + + redirect_to assignment_submission_similarities_url(params[:id]) +
    208. +
      + +
      +
    209. + + + + + + end +
    210. +
      + +
      +
    211. + + + + + + end +
    212. +
      + +
      +
    213. + + + + + + +
    214. +
      + +
      +
    215. + + + + + + # GET /assignments/1/submission_similarities/1/view_printable +
    216. +
      + +
      +
    217. + + + + + + def view_printable +
    218. +
      + +
      +
    219. + + + + + + @submission_similarity = SubmissionSimilarity.find(params["submission_similarity_id"]) +
    220. +
      + +
      +
    221. + + + + + + +
    222. +
      + +
      +
    223. + + + + + + render partial: "pair_report" +
    224. +
      + +
      +
    225. + + + + + + end +
    226. +
      + +
      +
    227. + + + + + + +
    228. +
      + +
      +
    229. + + + + + + # GET /assignments/1/view_printable_multiple?submission_similarity_ids=1,2,3 +
    230. +
      + +
      +
    231. + + + + + + def view_printable_multiple +
    232. +
      + +
      +
    233. + + + + + + @submission_similarity_ids = params["submission_similarity_ids"].split(',') +
    234. +
      + +
      +
    235. + + + + + + +
    236. +
      + +
      +
    237. + + + + + + render partial: "pair_report_multiple" +
    238. +
      + +
      +
    239. + + + + + + end +
    240. +
      + +
      +
    241. + + + + + + +
    242. +
      + +
      +
    243. + + + + + + # GET /assignments/1/submission_similarities/1 +
    244. +
      + +
      +
    245. + + + + + + def show +
    246. +
      + +
      +
    247. + + + + + + @submission_similarity = SubmissionSimilarity.find(params["id"]) +
    248. +
      + +
      +
    249. + + + + + + @submission1 = @submission_similarity.submission1 +
    250. +
      + +
      +
    251. + + + + + + @submission2 = @submission_similarity.submission2 +
    252. +
      + +
      +
    253. + + + + + + @student1 = @submission1.student +
    254. +
      + +
      +
    255. + + + + + + @student2 = @submission2.student +
    256. +
      + +
      +
    257. + + + + + + end +
    258. +
      + +
      +
    259. + + + + + + +
    260. +
      + +
      +
    261. + + + + + + # GET /students/1/submission_similarities/show_table_partial +
    262. +
      + +
      +
    263. + + + + + + def show_table_partial +
    264. +
      + +
      +
    265. + + + + + + locals = {} +
    266. +
      + +
      +
    267. + + + + + + locals[:student] = User.find(params["student_id"]) +
    268. +
      + +
      +
    269. + + + + + + if params["course_id"] +
    270. +
      + +
      +
    271. + + + + + + locals[:assignments] = Course.find(params["course_id"]).assignments +
    272. +
      + +
      +
    273. + + + + + + elsif params["assignment_id"] +
    274. +
      + +
      +
    275. + + + + + + locals[:assignments] = [ Assignment.find(params["assignment_id"]) ] +
    276. +
      + +
      +
    277. + + + + + + end +
    278. +
      + +
      +
    279. + + + + + + locals[:num_display] = params["num_display"].to_i +
    280. +
      + +
      +
    281. + + + + + + render partial: "submission_similarities_table", locals: locals +
    282. +
      + +
      +
    283. + + + + + + end +
    284. +
      + +
      +
    285. + + + + + + +
    286. +
      + +
      +
    287. + + + + + + # PUT /students/1/submission_similarities/1/confirm_as_plagiarism +
    288. +
      + +
      +
    289. + + + + + + def confirm_as_plagiarism +
    290. +
      + +
      +
    291. + + + + + + @submission_similarity = SubmissionSimilarity.find(params["submission_similarity_id"]) +
    292. +
      + +
      +
    293. + + + + + + @submission_similarity.status = SubmissionSimilarity::STATUS_CONFIRMED_AS_PLAGIARISM +
    294. +
      + +
      +
    295. + + + + + + if @submission_similarity.save +
    296. +
      + +
      +
    297. + + + + + + SubmissionLog.create { |sl| +
    298. +
      + +
      +
    299. + + + + + + sl.submission_similarity = @submission_similarity +
    300. +
      + +
      +
    301. + + + + + + sl.submission = @submission_similarity.submission1 +
    302. +
      + +
      +
    303. + + + + + + sl.marker = current_user +
    304. +
      + +
      +
    305. + + + + + + sl.log_type = SubmissionLog::TYPE_PAIR_CONFIRM_AS_PLAGIARISM +
    306. +
      + +
      +
    307. + + + + + + } +
    308. +
      + +
      +
    309. + + + + + + SubmissionLog.create { |sl| +
    310. +
      + +
      +
    311. + + + + + + sl.submission_similarity = @submission_similarity +
    312. +
      + +
      +
    313. + + + + + + sl.submission = @submission_similarity.submission2 +
    314. +
      + +
      +
    315. + + + + + + sl.marker = current_user +
    316. +
      + +
      +
    317. + + + + + + sl.log_type = SubmissionLog::TYPE_PAIR_CONFIRM_AS_PLAGIARISM +
    318. +
      + +
      +
    319. + + + + + + } +
    320. +
      + +
      +
    321. + + + + + + end +
    322. +
      + +
      +
    323. + + + + + + redirect_to assignment_submission_similarity_url(@assignment, @submission_similarity) +
    324. +
      + +
      +
    325. + + + + + + end +
    326. +
      + +
      +
    327. + + + + + + +
    328. +
      + +
      +
    329. + + + + + + # PUT /students/1/submission_similarities/1/suspect_as_plagiarism +
    330. +
      + +
      +
    331. + + + + + + def suspect_as_plagiarism +
    332. +
      + +
      +
    333. + + + + + + @submission_similarity = SubmissionSimilarity.find(params["submission_similarity_id"]) +
    334. +
      + +
      +
    335. + + + + + + @submission_similarity.status = SubmissionSimilarity::STATUS_SUSPECTED_AS_PLAGIARISM +
    336. +
      + +
      +
    337. + + + + + + if @submission_similarity.save +
    338. +
      + +
      +
    339. + + + + + + mark_student_as_not_guilty(@submission_similarity.submission1, @submission_similarity) +
    340. +
      + +
      +
    341. + + + + + + mark_student_as_not_guilty(@submission_similarity.submission2, @submission_similarity) +
    342. +
      + +
      +
    343. + + + + + + SubmissionLog.create { |sl| +
    344. +
      + +
      +
    345. + + + + + + sl.submission_similarity = @submission_similarity +
    346. +
      + +
      +
    347. + + + + + + sl.submission = @submission_similarity.submission1 +
    348. +
      + +
      +
    349. + + + + + + sl.marker = current_user +
    350. +
      + +
      +
    351. + + + + + + sl.log_type = SubmissionLog::TYPE_PAIR_SUSPECT_AS_PLAGIARISM +
    352. +
      + +
      +
    353. + + + + + + } +
    354. +
      + +
      +
    355. + + + + + + SubmissionLog.create { |sl| +
    356. +
      + +
      +
    357. + + + + + + sl.submission_similarity = @submission_similarity +
    358. +
      + +
      +
    359. + + + + + + sl.submission = @submission_similarity.submission2 +
    360. +
      + +
      +
    361. + + + + + + sl.marker = current_user +
    362. +
      + +
      +
    363. + + + + + + sl.log_type = SubmissionLog::TYPE_PAIR_SUSPECT_AS_PLAGIARISM +
    364. +
      + +
      +
    365. + + + + + + } +
    366. +
      + +
      +
    367. + + + + + + end +
    368. +
      + +
      +
    369. + + + + + + redirect_to assignment_submission_similarity_url(@assignment, @submission_similarity) +
    370. +
      + +
      +
    371. + + + + + + end +
    372. +
      + +
      +
    373. + + + + + + +
    374. +
      + +
      +
    375. + + + + + + # PUT /students/1/submission_similarities/1/unmark_as_plagiarism +
    376. +
      + +
      +
    377. + + + + + + def unmark_as_plagiarism +
    378. +
      + +
      +
    379. + + + + + + @submission_similarity = SubmissionSimilarity.find(params["submission_similarity_id"]) +
    380. +
      + +
      +
    381. + + + + + + @submission_similarity.status = SubmissionSimilarity::STATUS_NOT_PLAGIARISM +
    382. +
      + +
      +
    383. + + + + + + if @submission_similarity.save +
    384. +
      + +
      +
    385. + + + + + + mark_student_as_not_guilty(@submission_similarity.submission1, @submission_similarity) +
    386. +
      + +
      +
    387. + + + + + + mark_student_as_not_guilty(@submission_similarity.submission2, @submission_similarity) +
    388. +
      + +
      +
    389. + + + + + + SubmissionLog.create { |sl| +
    390. +
      + +
      +
    391. + + + + + + sl.submission_similarity = @submission_similarity +
    392. +
      + +
      +
    393. + + + + + + sl.submission = @submission_similarity.submission1 +
    394. +
      + +
      +
    395. + + + + + + sl.marker = current_user +
    396. +
      + +
      +
    397. + + + + + + sl.log_type = SubmissionLog::TYPE_PAIR_UNMARK_AS_PLAGIARISM +
    398. +
      + +
      +
    399. + + + + + + } +
    400. +
      + +
      +
    401. + + + + + + SubmissionLog.create { |sl| +
    402. +
      + +
      +
    403. + + + + + + sl.submission_similarity = @submission_similarity +
    404. +
      + +
      +
    405. + + + + + + sl.submission = @submission_similarity.submission2 +
    406. +
      + +
      +
    407. + + + + + + sl.marker = current_user +
    408. +
      + +
      +
    409. + + + + + + sl.log_type = SubmissionLog::TYPE_PAIR_UNMARK_AS_PLAGIARISM +
    410. +
      + +
      +
    411. + + + + + + } +
    412. +
      + +
      +
    413. + + + + + + end +
    414. +
      + +
      +
    415. + + + + + + redirect_to assignment_submission_similarity_url(@assignment, @submission_similarity) +
    416. +
      + +
      +
    417. + + + + + + end +
    418. +
      + +
      +
    419. + + + + + + +
    420. +
      + +
      +
    421. + + + + + + private +
    422. +
      + +
      +
    423. + + + + + + +
    424. +
      + +
      +
    425. + + + + + + def mark_student_as_not_guilty(submission, submission_similarity) +
    426. +
      + +
      +
    427. + + + + + + return unless submission.is_plagiarism +
    428. +
      + +
      +
    429. + + + + + + submission.is_plagiarism = false +
    430. +
      + +
      +
    431. + + + + + + if submission.save +
    432. +
      + +
      +
    433. + + + + + + SubmissionLog.create { |sl| +
    434. +
      + +
      +
    435. + + + + + + sl.submission_similarity = submission_similarity +
    436. +
      + +
      +
    437. + + + + + + sl.submission = submission +
    438. +
      + +
      +
    439. + + + + + + sl.marker = current_user +
    440. +
      + +
      +
    441. + + + + + + sl.log_type = SubmissionLog::TYPE_STUDENT_MARK_AS_NOT_GUILTY +
    442. +
      + +
      +
    443. + + + + + + } +
    444. +
      + +
      +
    445. + + + + + + end +
    446. +
      + +
      +
    447. + + + + + + end +
    448. +
      + +
      +
    449. + + + + + + end +
    450. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/submission_similarity_mappings_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 15 relevant lines. + 0 lines covered and + 15 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionSimilarityMappingsController < ApplicationController +
    36. +
      + +
      +
    37. + + + + + + end +
    38. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/submission_similarity_processes_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 15 relevant lines. + 0 lines covered and + 15 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionSimilarityProcessesController < ApplicationController +
    36. +
      + +
      +
    37. + + + + + + end +
    38. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/submissions_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 66 relevant lines. + 0 lines covered and + 66 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionsController < ApplicationController +
    36. +
      + +
      +
    37. + + + + + + before_action { |controller| +
    38. +
      + +
      +
    39. + + + + + + @course = nil +
    40. +
      + +
      +
    41. + + + + + + if params[:assignment_id] +
    42. +
      + +
      +
    43. + + + + + + @assignment = Assignment.find(params[:assignment_id]) +
    44. +
      + +
      +
    45. + + + + + + @course = @assignment.course +
    46. +
      + +
      +
    47. + + + + + + elsif params["submission_id"] +
    48. +
      + +
      +
    49. + + + + + + @submission = Submission.find(params["submission_id"]) +
    50. +
      + +
      +
    51. + + + + + + @course = @submission.course +
    52. +
      + +
      +
    53. + + + + + + end +
    54. +
      + +
      +
    55. + + + + + + +
    56. +
      + +
      +
    57. + + + + + + if @course +
    58. +
      + +
      +
    59. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, +
    60. +
      + +
      +
    61. + + + + + + course: @course, +
    62. +
      + +
      +
    63. + + + + + + only: [ ] +
    64. +
      + +
      +
    65. + + + + + + end +
    66. +
      + +
      +
    67. + + + + + + } +
    68. +
      + +
      +
    69. + + + + + + +
    70. +
      + +
      +
    71. + + + + + + # GET /assignments/id/list +
    72. +
      + +
      +
    73. + + + + + + def index +
    74. +
      + +
      +
    75. + + + + + + @submissions = @assignment.submissions +
    76. +
      + +
      +
    77. + + + + + + +
    78. +
      + +
      +
    79. + + + + + + respond_to do |format| +
    80. +
      + +
      +
    81. + + + + + + format.json { +
    82. +
      + +
      +
    83. + + + + + + render json: @submissions.collect { |s| +
    84. +
      + +
      +
    85. + + + + + + { id: s.id, student_id: s.student.id, student_name: s.student_name } +
    86. +
      + +
      +
    87. + + + + + + } +
    88. +
      + +
      +
    89. + + + + + + } +
    90. +
      + +
      +
    91. + + + + + + end +
    92. +
      + +
      +
    93. + + + + + + end +
    94. +
      + +
      +
    95. + + + + + + +
    96. +
      + +
      +
    97. + + + + + + def show_log +
    98. +
      + +
      +
    99. + + + + + + end +
    100. +
      + +
      +
    101. + + + + + + +
    102. +
      + +
      +
    103. + + + + + + def mark_as_guilty +
    104. +
      + +
      +
    105. + + + + + + @submission_similarity = SubmissionSimilarity.find(params["submission_similarity_id"]) +
    106. +
      + +
      +
    107. + + + + + + @submission.is_plagiarism = true +
    108. +
      + +
      +
    109. + + + + + + if @submission.save +
    110. +
      + +
      +
    111. + + + + + + SubmissionLog.create { |sl| +
    112. +
      + +
      +
    113. + + + + + + sl.submission_similarity = @submission_similarity +
    114. +
      + +
      +
    115. + + + + + + sl.submission = @submission +
    116. +
      + +
      +
    117. + + + + + + sl.marker = current_user +
    118. +
      + +
      +
    119. + + + + + + sl.log_type = SubmissionLog::TYPE_STUDENT_MARK_AS_GUILTY +
    120. +
      + +
      +
    121. + + + + + + } +
    122. +
      + +
      +
    123. + + + + + + end +
    124. +
      + +
      +
    125. + + + + + + # render body :nil, status: 200 +
    126. +
      + +
      +
    127. + + + + + + end +
    128. +
      + +
      +
    129. + + + + + + +
    130. +
      + +
      +
    131. + + + + + + def mark_as_not_guilty +
    132. +
      + +
      +
    133. + + + + + + @submission_similarity = SubmissionSimilarity.find(params["submission_similarity_id"]) +
    134. +
      + +
      +
    135. + + + + + + @submission.is_plagiarism = false +
    136. +
      + +
      +
    137. + + + + + + if @submission.save +
    138. +
      + +
      +
    139. + + + + + + SubmissionLog.create { |sl| +
    140. +
      + +
      +
    141. + + + + + + sl.submission_similarity = @submission_similarity +
    142. +
      + +
      +
    143. + + + + + + sl.submission = @submission +
    144. +
      + +
      +
    145. + + + + + + sl.marker = current_user +
    146. +
      + +
      +
    147. + + + + + + sl.log_type = SubmissionLog::TYPE_STUDENT_MARK_AS_NOT_GUILTY +
    148. +
      + +
      +
    149. + + + + + + } +
    150. +
      + +
      +
    151. + + + + + + end +
    152. +
      + +
      +
    153. + + + + + + # render body: nil, status: 200 +
    154. +
      + +
      +
    155. + + + + + + end +
    156. +
      + +
      +
    157. + + + + + + end +
    158. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/user_course_memberships_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 79 relevant lines. + 0 lines covered and + 79 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class UserCourseMembershipsController < ApplicationController +
    36. +
      + +
      +
    37. + + + + + + before_action { |controller| +
    38. +
      + +
      +
    39. + + + + + + if params[:course_id] +
    40. +
      + +
      +
    41. + + + + + + @course = Course.find(params[:course_id]) +
    42. +
      + +
      +
    43. + + + + + + end +
    44. +
      + +
      +
    45. + + + + + + +
    46. +
      + +
      +
    47. + + + + + + if @course +
    48. +
      + +
      +
    49. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_TEACHING_STAFF, +
    50. +
      + +
      +
    51. + + + + + + course: @course, +
    52. +
      + +
      +
    53. + + + + + + only: [ :index, :new, :create, :edit, :update, :destroy ] +
    54. +
      + +
      +
    55. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_TEACHING_ASSISTANT, +
    56. +
      + +
      +
    57. + + + + + + course: @course, +
    58. +
      + +
      +
    59. + + + + + + only: [ :index ] +
    60. +
      + +
      +
    61. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, +
    62. +
      + +
      +
    63. + + + + + + course: @course, +
    64. +
      + +
      +
    65. + + + + + + only: [ ] +
    66. +
      + +
      +
    67. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_GUEST, +
    68. +
      + +
      +
    69. + + + + + + course: @course, +
    70. +
      + +
      +
    71. + + + + + + only: [ ] +
    72. +
      + +
      +
    73. + + + + + + end +
    74. +
      + +
      +
    75. + + + + + + } +
    76. +
      + +
      +
    77. + + + + + + +
    78. +
      + +
      +
    79. + + + + + + # GET /courses/:course_id/user_course_memberships +
    80. +
      + +
      +
    81. + + + + + + def index +
    82. +
      + +
      +
    83. + + + + + + @course = Course.find(params[:course_id]) +
    84. +
      + +
      +
    85. + + + + + + @staff = @course.staff +
    86. +
      + +
      +
    87. + + + + + + @teaching_assistants = @course.teaching_assistants +
    88. +
      + +
      +
    89. + + + + + + @students = @course.students +
    90. +
      + +
      +
    91. + + + + + + @guests = @course.guests +
    92. +
      + +
      +
    93. + + + + + + end +
    94. +
      + +
      +
    95. + + + + + + +
    96. +
      + +
      +
    97. + + + + + + # GET /courses/:course_id/user_course_memberships/new +
    98. +
      + +
      +
    99. + + + + + + def new +
    100. +
      + +
      +
    101. + + + + + + @course = Course.find_by_id(params[:course_id]) +
    102. +
      + +
      +
    103. + + + + + + @user_course_membership = UserCourseMembership.new +
    104. +
      + +
      +
    105. + + + + + + end +
    106. +
      + +
      +
    107. + + + + + + +
    108. +
      + +
      +
    109. + + + + + + # POST /courses/:course_id/user_course_memberships +
    110. +
      + +
      +
    111. + + + + + + def create +
    112. +
      + +
      +
    113. + + + + + + @user = User.find_by_email(params[:user_course_membership][:user_email]) +
    114. +
      + +
      +
    115. + + + + + + +
    116. +
      + +
      +
    117. + + + + + + @course = Course.find_by_id(params[:course_id]) +
    118. +
      + +
      +
    119. + + + + + + course_role = params[:user_course_membership][:course_role] +
    120. +
      + +
      +
    121. + + + + + + @user_course_membership = UserCourseMembership.new { |ucm| +
    122. +
      + +
      +
    123. + + + + + + ucm.course = @course +
    124. +
      + +
      +
    125. + + + + + + ucm.user = @user +
    126. +
      + +
      +
    127. + + + + + + ucm.role = course_role +
    128. +
      + +
      +
    129. + + + + + + } +
    130. +
      + +
      +
    131. + + + + + + +
    132. +
      + +
      +
    133. + + + + + + if @user_course_membership.save +
    134. +
      + +
      +
    135. + + + + + + return redirect_to course_user_course_memberships_url, notice: "User was successfully added to the course." +
    136. +
      + +
      +
    137. + + + + + + else +
    138. +
      + +
      +
    139. + + + + + + return render action: "new" +
    140. +
      + +
      +
    141. + + + + + + end +
    142. +
      + +
      +
    143. + + + + + + end +
    144. +
      + +
      +
    145. + + + + + + +
    146. +
      + +
      +
    147. + + + + + + # GET /courses/:course_id/user_course_memberships/:id/edit +
    148. +
      + +
      +
    149. + + + + + + def edit +
    150. +
      + +
      +
    151. + + + + + + @course = Course.find_by_id(params[:course_id]) +
    152. +
      + +
      +
    153. + + + + + + @user_course_membership = UserCourseMembership.find_by_id(params[:id]) +
    154. +
      + +
      +
    155. + + + + + + end +
    156. +
      + +
      +
    157. + + + + + + +
    158. +
      + +
      +
    159. + + + + + + # PATCH /courses/:course_id/user_course_memberships/:id +
    160. +
      + +
      +
    161. + + + + + + def update +
    162. +
      + +
      +
    163. + + + + + + @user_course_membership = UserCourseMembership.find_by_id(params[:id]) +
    164. +
      + +
      +
    165. + + + + + + course_role = params[:user_course_membership][:course_role] +
    166. +
      + +
      +
    167. + + + + + + @user_course_membership.role = course_role +
    168. +
      + +
      +
    169. + + + + + + if @user_course_membership.save +
    170. +
      + +
      +
    171. + + + + + + return redirect_to course_user_course_memberships_url, notice: "User course role was successfully updated." +
    172. +
      + +
      +
    173. + + + + + + else +
    174. +
      + +
      +
    175. + + + + + + return render action: "edit" +
    176. +
      + +
      +
    177. + + + + + + end +
    178. +
      + +
      +
    179. + + + + + + end +
    180. +
      + +
      +
    181. + + + + + + +
    182. +
      + +
      +
    183. + + + + + + # DELETE /courses/:course_id/user_course_memberships/:id +
    184. +
      + +
      +
    185. + + + + + + def destroy +
    186. +
      + +
      +
    187. + + + + + + @user_course_membership = UserCourseMembership.find_by_id(params[:id]) +
    188. +
      + +
      +
    189. + + + + + + @user_course_membership.destroy +
    190. +
      + +
      +
    191. + + + + + + +
    192. +
      + +
      +
    193. + + + + + + redirect_to course_user_course_memberships_url, notice: "User was successfully removed from the course." +
    194. +
      + +
      +
    195. + + + + + + end +
    196. +
      + +
      +
    197. + + + + + + end +
    198. +
      + +
      +
    199. + + + + + + +
    200. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/users/confirmations_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 8 relevant lines. + 0 lines covered and + 8 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + # frozen_string_literal: true +
    2. +
      + +
      +
    3. + + + + + + +
    4. +
      + +
      +
    5. + + + + + + class Users::ConfirmationsController < Devise::ConfirmationsController +
    6. +
      + +
      +
    7. + + + + + + # GET /resource/confirmation/new +
    8. +
      + +
      +
    9. + + + + + + def new +
    10. +
      + +
      +
    11. + + + + + + super +
    12. +
      + +
      +
    13. + + + + + + +
    14. +
      + +
      +
    15. + + + + + + end +
    16. +
      + +
      +
    17. + + + + + + +
    18. +
      + +
      +
    19. + + + + + + # POST /resource/confirmation +
    20. +
      + +
      +
    21. + + + + + + def create +
    22. +
      + +
      +
    23. + + + + + + super +
    24. +
      + +
      +
    25. + + + + + + end +
    26. +
      + +
      +
    27. + + + + + + +
    28. +
      + +
      +
    29. + + + + + + # GET /resource/confirmation?confirmation_token=abcdef +
    30. +
      + +
      +
    31. + + + + + + # def show +
    32. +
      + +
      +
    33. + + + + + + # super +
    34. +
      + +
      +
    35. + + + + + + # end +
    36. +
      + +
      +
    37. + + + + + + +
    38. +
      + +
      +
    39. + + + + + + # protected +
    40. +
      + +
      +
    41. + + + + + + +
    42. +
      + +
      +
    43. + + + + + + # The path used after resending confirmation instructions. +
    44. +
      + +
      +
    45. + + + + + + # def after_resending_confirmation_instructions_path_for(resource_name) +
    46. +
      + +
      +
    47. + + + + + + # super(resource_name) +
    48. +
      + +
      +
    49. + + + + + + # end +
    50. +
      + +
      +
    51. + + + + + + +
    52. +
      + +
      +
    53. + + + + + + # The path used after confirmation. +
    54. +
      + +
      +
    55. + + + + + + # def after_confirmation_path_for(resource_name, resource) +
    56. +
      + +
      +
    57. + + + + + + # super(resource_name, resource) +
    58. +
      + +
      +
    59. + + + + + + # end +
    60. +
      + +
      +
    61. + + + + + + end +
    62. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/users/omniauth_callbacks_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 37 relevant lines. + 0 lines covered and + 37 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + # frozen_string_literal: true +
    2. +
      + +
      +
    3. + + + + + + +
    4. +
      + +
      +
    5. + + + + + + class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController +
    6. +
      + +
      +
    7. + + + + + + # You should configure your model like this: +
    8. +
      + +
      +
    9. + + + + + + # devise :omniauthable, omniauth_providers: [:twitter] +
    10. +
      + +
      +
    11. + + + + + + +
    12. +
      + +
      +
    13. + + + + + + # You should also create an action method in this controller like this: +
    14. +
      + +
      +
    15. + + + + + + # def twitter +
    16. +
      + +
      +
    17. + + + + + + # end +
    18. +
      + +
      +
    19. + + + + + + +
    20. +
      + +
      +
    21. + + + + + + # More info at: +
    22. +
      + +
      +
    23. + + + + + + # https://github.com/heartcombo/devise#omniauth +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + # GET|POST /resource/auth/twitter +
    28. +
      + +
      +
    29. + + + + + + # def passthru +
    30. +
      + +
      +
    31. + + + + + + # super +
    32. +
      + +
      +
    33. + + + + + + # end +
    34. +
      + +
      +
    35. + + + + + + +
    36. +
      + +
      +
    37. + + + + + + # GET|POST /users/auth/twitter/callback +
    38. +
      + +
      +
    39. + + + + + + # def failure +
    40. +
      + +
      +
    41. + + + + + + # super +
    42. +
      + +
      +
    43. + + + + + + # end +
    44. +
      + +
      +
    45. + + + + + + +
    46. +
      + +
      +
    47. + + + + + + # protected +
    48. +
      + +
      +
    49. + + + + + + +
    50. +
      + +
      +
    51. + + + + + + # The path used when OmniAuth fails +
    52. +
      + +
      +
    53. + + + + + + # def after_omniauth_failure_path_for(scope) +
    54. +
      + +
      +
    55. + + + + + + # super(scope) +
    56. +
      + +
      +
    57. + + + + + + # end +
    58. +
      + +
      +
    59. + + + + + + +
    60. +
      + +
      +
    61. + + + + + + def google_oauth2 +
    62. +
      + +
      +
    63. + + + + + + user = User.from_omniauth(auth) +
    64. +
      + +
      +
    65. + + + + + + # byebug +
    66. +
      + +
      +
    67. + + + + + + if user.present? +
    68. +
      + +
      +
    69. + + + + + + found_user = User.find_by(email: auth.info.email) +
    70. +
      + +
      +
    71. + + + + + + if found_user.present? +
    72. +
      + +
      +
    73. + + + + + + if found_user.is_admin_approved? +
    74. +
      + +
      +
    75. + + + + + + sign_out_all_scopes +
    76. +
      + +
      +
    77. + + + + + + flash[:success] = t 'devise.omniauth_callbacks.success', kind: 'Google' +
    78. +
      + +
      +
    79. + + + + + + sign_in_and_redirect user, event: :authentication +
    80. +
      + +
      +
    81. + + + + + + else +
    82. +
      + +
      +
    83. + + + + + + redirect_to new_user_session_url, alert: "Your account is still being processed." +
    84. +
      + +
      +
    85. + + + + + + end +
    86. +
      + +
      +
    87. + + + + + + else +
    88. +
      + +
      +
    89. + + + + + + user.name = user.full_name +
    90. +
      + +
      +
    91. + + + + + + user.confirm +
    92. +
      + +
      +
    93. + + + + + + user.save +
    94. +
      + +
      +
    95. + + + + + + redirect_to root_url, notice: "Welcome to SSID! Your account is being processed. An email will be sent to you once your account is approved." +
    96. +
      + +
      +
    97. + + + + + + end +
    98. +
      + +
      +
    99. + + + + + + else +
    100. +
      + +
      +
    101. + + + + + + flash[:alert] = +
    102. +
      + +
      +
    103. + + + + + + t 'devise.omniauth_callbacks.failure', kind: 'Google', reason: "#{auth.info.email} is not authorized." +
    104. +
      + +
      +
    105. + + + + + + redirect_to new_user_session_path +
    106. +
      + +
      +
    107. + + + + + + end +
    108. +
      + +
      +
    109. + + + + + + end +
    110. +
      + +
      +
    111. + + + + + + protected +
    112. +
      + +
      +
    113. + + + + + + def after_omniauth_failure_path_for(_scope) +
    114. +
      + +
      +
    115. + + + + + + new_user_session_path +
    116. +
      + +
      +
    117. + + + + + + end +
    118. +
      + +
      +
    119. + + + + + + def after_sign_in_path_for(resource_or_scope) +
    120. +
      + +
      +
    121. + + + + + + stored_location_for(resource_or_scope) || root_path +
    122. +
      + +
      +
    123. + + + + + + end +
    124. +
      + +
      +
    125. + + + + + + private +
    126. +
      + +
      +
    127. + + + + + + def auth +
    128. +
      + +
      +
    129. + + + + + + @auth ||= request.env['omniauth.auth'] +
    130. +
      + +
      +
    131. + + + + + + end +
    132. +
      + +
      +
    133. + + + + + + end +
    134. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/users/passwords_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 2 relevant lines. + 0 lines covered and + 2 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + # frozen_string_literal: true +
    2. +
      + +
      +
    3. + + + + + + +
    4. +
      + +
      +
    5. + + + + + + class Users::PasswordsController < Devise::PasswordsController +
    6. +
      + +
      +
    7. + + + + + + # GET /resource/password/new +
    8. +
      + +
      +
    9. + + + + + + # def new +
    10. +
      + +
      +
    11. + + + + + + # super +
    12. +
      + +
      +
    13. + + + + + + # end +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + # POST /resource/password +
    18. +
      + +
      +
    19. + + + + + + # def create +
    20. +
      + +
      +
    21. + + + + + + # super +
    22. +
      + +
      +
    23. + + + + + + # end +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + # GET /resource/password/edit?reset_password_token=abcdef +
    28. +
      + +
      +
    29. + + + + + + # def edit +
    30. +
      + +
      +
    31. + + + + + + # super +
    32. +
      + +
      +
    33. + + + + + + # end +
    34. +
      + +
      +
    35. + + + + + + +
    36. +
      + +
      +
    37. + + + + + + # PUT /resource/password +
    38. +
      + +
      +
    39. + + + + + + # def update +
    40. +
      + +
      +
    41. + + + + + + # super +
    42. +
      + +
      +
    43. + + + + + + # end +
    44. +
      + +
      +
    45. + + + + + + +
    46. +
      + +
      +
    47. + + + + + + # protected +
    48. +
      + +
      +
    49. + + + + + + +
    50. +
      + +
      +
    51. + + + + + + # def after_resetting_password_path_for(resource) +
    52. +
      + +
      +
    53. + + + + + + # super(resource) +
    54. +
      + +
      +
    55. + + + + + + # end +
    56. +
      + +
      +
    57. + + + + + + +
    58. +
      + +
      +
    59. + + + + + + # The path used after sending reset password instructions +
    60. +
      + +
      +
    61. + + + + + + # def after_sending_reset_password_instructions_path_for(resource_name) +
    62. +
      + +
      +
    63. + + + + + + # super(resource_name) +
    64. +
      + +
      +
    65. + + + + + + # end +
    66. +
      + +
      +
    67. + + + + + + end +
    68. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/users/registrations_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 23 relevant lines. + 0 lines covered and + 23 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + # frozen_string_literal: true +
    2. +
      + +
      +
    3. + + + + + + +
    4. +
      + +
      +
    5. + + + + + + class Users::RegistrationsController < Devise::RegistrationsController +
    6. +
      + +
      +
    7. + + + + + + before_action :configure_sign_up_params, only: [:create] +
    8. +
      + +
      +
    9. + + + + + + before_action :configure_account_update_params, only: [:update] +
    10. +
      + +
      +
    11. + + + + + + +
    12. +
      + +
      +
    13. + + + + + + # GET /resource/sign_up +
    14. +
      + +
      +
    15. + + + + + + def new +
    16. +
      + +
      +
    17. + + + + + + super +
    18. +
      + +
      +
    19. + + + + + + end +
    20. +
      + +
      +
    21. + + + + + + +
    22. +
      + +
      +
    23. + + + + + + # POST /resource +
    24. +
      + +
      +
    25. + + + + + + # def create +
    26. +
      + +
      +
    27. + + + + + + # super +
    28. +
      + +
      +
    29. + + + + + + # end +
    30. +
      + +
      +
    31. + + + + + + +
    32. +
      + +
      +
    33. + + + + + + # GET /resource/edit +
    34. +
      + +
      +
    35. + + + + + + # def edit +
    36. +
      + +
      +
    37. + + + + + + # super +
    38. +
      + +
      +
    39. + + + + + + # end +
    40. +
      + +
      +
    41. + + + + + + +
    42. +
      + +
      +
    43. + + + + + + # PUT /resource +
    44. +
      + +
      +
    45. + + + + + + # def update +
    46. +
      + +
      +
    47. + + + + + + # super +
    48. +
      + +
      +
    49. + + + + + + # end +
    50. +
      + +
      +
    51. + + + + + + +
    52. +
      + +
      +
    53. + + + + + + # DELETE /resource +
    54. +
      + +
      +
    55. + + + + + + # def destroy +
    56. +
      + +
      +
    57. + + + + + + # super +
    58. +
      + +
      +
    59. + + + + + + # end +
    60. +
      + +
      +
    61. + + + + + + +
    62. +
      + +
      +
    63. + + + + + + # GET /resource/cancel +
    64. +
      + +
      +
    65. + + + + + + # Forces the session data which is usually expired after sign +
    66. +
      + +
      +
    67. + + + + + + # in to be expired now. This is useful if the user wants to +
    68. +
      + +
      +
    69. + + + + + + # cancel oauth signing in/up in the middle of the process, +
    70. +
      + +
      +
    71. + + + + + + # removing all OAuth session data. +
    72. +
      + +
      +
    73. + + + + + + # def cancel +
    74. +
      + +
      +
    75. + + + + + + # super +
    76. +
      + +
      +
    77. + + + + + + # end +
    78. +
      + +
      +
    79. + + + + + + +
    80. +
      + +
      +
    81. + + + + + + def update_resource(resource, params) +
    82. +
      + +
      +
    83. + + + + + + if resource.provider == 'google_oauth2' +
    84. +
      + +
      +
    85. + + + + + + params.delete('current_password') +
    86. +
      + +
      +
    87. + + + + + + resource.password = params['password'] +
    88. +
      + +
      +
    89. + + + + + + resource.update_without_password(params) +
    90. +
      + +
      +
    91. + + + + + + else +
    92. +
      + +
      +
    93. + + + + + + resource.update_with_password(params) +
    94. +
      + +
      +
    95. + + + + + + end +
    96. +
      + +
      +
    97. + + + + + + end +
    98. +
      + +
      +
    99. + + + + + + +
    100. +
      + +
      +
    101. + + + + + + protected +
    102. +
      + +
      +
    103. + + + + + + +
    104. +
      + +
      +
    105. + + + + + + # If you have extra params to permit, append them to the sanitizer. +
    106. +
      + +
      +
    107. + + + + + + def configure_sign_up_params +
    108. +
      + +
      +
    109. + + + + + + devise_parameter_sanitizer.permit(:sign_up, keys: [ :name, :full_name]) +
    110. +
      + +
      +
    111. + + + + + + end +
    112. +
      + +
      +
    113. + + + + + + +
    114. +
      + +
      +
    115. + + + + + + # If you have extra params to permit, append them to the sanitizer. +
    116. +
      + +
      +
    117. + + + + + + def configure_account_update_params +
    118. +
      + +
      +
    119. + + + + + + devise_parameter_sanitizer.permit(:account_update, keys: [:name, :full_name]) +
    120. +
      + +
      +
    121. + + + + + + end +
    122. +
      + +
      +
    123. + + + + + + +
    124. +
      + +
      +
    125. + + + + + + # The path used after sign up. +
    126. +
      + +
      +
    127. + + + + + + # def after_sign_up_path_for(resource) +
    128. +
      + +
      +
    129. + + + + + + # super(resource) +
    130. +
      + +
      +
    131. + + + + + + # end +
    132. +
      + +
      +
    133. + + + + + + +
    134. +
      + +
      +
    135. + + + + + + # The path used after sign up for inactive accounts. +
    136. +
      + +
      +
    137. + + + + + + # def after_inactive_sign_up_path_for(resource) +
    138. +
      + +
      +
    139. + + + + + + # super(resource) +
    140. +
      + +
      +
    141. + + + + + + # end +
    142. +
      + +
      +
    143. + + + + + + end +
    144. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/users/sessions_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 36 relevant lines. + 0 lines covered and + 36 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + # frozen_string_literal: true +
    2. +
      + +
      +
    3. + + + + + + +
    4. +
      + +
      +
    5. + + + + + + class Users::SessionsController < Devise::SessionsController +
    6. +
      + +
      +
    7. + + + + + + # before_action :configure_sign_in_params, only: [:create] +
    8. +
      + +
      +
    9. + + + + + + +
    10. +
      + +
      +
    11. + + + + + + # GET /resource/sign_in +
    12. +
      + +
      +
    13. + + + + + + # def new +
    14. +
      + +
      +
    15. + + + + + + # super +
    16. +
      + +
      +
    17. + + + + + + # end +
    18. +
      + +
      +
    19. + + + + + + +
    20. +
      + +
      +
    21. + + + + + + # POST /resource/sign_in +
    22. +
      + +
      +
    23. + + + + + + # def create +
    24. +
      + +
      +
    25. + + + + + + # super +
    26. +
      + +
      +
    27. + + + + + + # end +
    28. +
      + +
      +
    29. + + + + + + +
    30. +
      + +
      +
    31. + + + + + + # DELETE /resource/sign_out +
    32. +
      + +
      +
    33. + + + + + + # def destroy +
    34. +
      + +
      +
    35. + + + + + + # super +
    36. +
      + +
      +
    37. + + + + + + # end +
    38. +
      + +
      +
    39. + + + + + + +
    40. +
      + +
      +
    41. + + + + + + +
    42. +
      + +
      +
    43. + + + + + + skip_before_action :authenticate_user!, only: [:new, :create, :index, :guide] +
    44. +
      + +
      +
    45. + + + + + + +
    46. +
      + +
      +
    47. + + + + + + def index +
    48. +
      + +
      +
    49. + + + + + + render "layouts/cover" +
    50. +
      + +
      +
    51. + + + + + + end +
    52. +
      + +
      +
    53. + + + + + + +
    54. +
      + +
      +
    55. + + + + + + def new +
    56. +
      + +
      +
    57. + + + + + + super +
    58. +
      + +
      +
    59. + + + + + + end +
    60. +
      + +
      +
    61. + + + + + + +
    62. +
      + +
      +
    63. + + + + + + def create +
    64. +
      + +
      +
    65. + + + + + + user = User.find_by_email(params[:user][:email]) +
    66. +
      + +
      +
    67. + + + + + + if user and user.valid_password?(params[:user][:password]) and user.is_admin_approved +
    68. +
      + +
      +
    69. + + + + + + if user.confirmed? +
    70. +
      + +
      +
    71. + + + + + + sign_in user +
    72. +
      + +
      +
    73. + + + + + + redirect_to root_url +
    74. +
      + +
      +
    75. + + + + + + else +
    76. +
      + +
      +
    77. + + + + + + redirect_to new_user_session_url, alert: "Your account is not activated. Please check your email for the activation link." +
    78. +
      + +
      +
    79. + + + + + + end +
    80. +
      + +
      +
    81. + + + + + + else +
    82. +
      + +
      +
    83. + + + + + + redirect_to new_user_session_url, alert: "Invalid user/password combination or user account is still being processed." +
    84. +
      + +
      +
    85. + + + + + + end +
    86. +
      + +
      +
    87. + + + + + + end +
    88. +
      + +
      +
    89. + + + + + + +
    90. +
      + +
      +
    91. + + + + + + +
    92. +
      + +
      +
    93. + + + + + + def destroy +
    94. +
      + +
      +
    95. + + + + + + sign_out current_user +
    96. +
      + +
      +
    97. + + + + + + redirect_to root_url +
    98. +
      + +
      +
    99. + + + + + + end +
    100. +
      + +
      +
    101. + + + + + + +
    102. +
      + +
      +
    103. + + + + + + def guide +
    104. +
      + +
      +
    105. + + + + + + render "layouts/guide" +
    106. +
      + +
      +
    107. + + + + + + end +
    108. +
      + +
      +
    109. + + + + + + +
    110. +
      + +
      +
    111. + + + + + + protected +
    112. +
      + +
      +
    113. + + + + + + +
    114. +
      + +
      +
    115. + + + + + + # # If you have extra params to permit, append them to the sanitizer. +
    116. +
      + +
      +
    117. + + + + + + # def configure_sign_in_params +
    118. +
      + +
      +
    119. + + + + + + # devise_parameter_sanitizer.permit(:sign_in, keys: [:email, :password, :remember_me]) +
    120. +
      + +
      +
    121. + + + + + + # end +
    122. +
      + +
      +
    123. + + + + + + +
    124. +
      + +
      +
    125. + + + + + + def after_sign_out_path_for(_resource_or_scope) +
    126. +
      + +
      +
    127. + + + + + + new_user_session_path +
    128. +
      + +
      +
    129. + + + + + + end +
    130. +
      + +
      +
    131. + + + + + + def after_sign_in_path_for(resource_or_scope) +
    132. +
      + +
      +
    133. + + + + + + stored_location_for(resource_or_scope) || root_path +
    134. +
      + +
      +
    135. + + + + + + end +
    136. +
      + +
      +
    137. + + + + + + end +
    138. +
      + +
    +
    +
    + + +
    +
    +

    app/controllers/visualize_controller.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 35 relevant lines. + 0 lines covered and + 35 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class VisualizeController < ApplicationController +
    36. +
      + +
      +
    37. + + + + + + before_action { |controller| +
    38. +
      + +
      +
    39. + + + + + + if params[:course_id] +
    40. +
      + +
      +
    41. + + + + + + @course = Course.find(params[:course_id]) +
    42. +
      + +
      +
    43. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, +
    44. +
      + +
      +
    45. + + + + + + course: @course, +
    46. +
      + +
      +
    47. + + + + + + only: [ ] +
    48. +
      + +
      +
    49. + + + + + + controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_GUEST, +
    50. +
      + +
      +
    51. + + + + + + course: @course, +
    52. +
      + +
      +
    53. + + + + + + only: [ ] +
    54. +
      + +
      +
    55. + + + + + + end +
    56. +
      + +
      +
    57. + + + + + + } +
    58. +
      + +
      +
    59. + + + + + + +
    60. +
      + +
      +
    61. + + + + + + # GET /courses/1/visualize +
    62. +
      + +
      +
    63. + + + + + + def index +
    64. +
      + +
      +
    65. + + + + + + end +
    66. +
      + +
      +
    67. + + + + + + +
    68. +
      + +
      +
    69. + + + + + + # GET /courses/1/visualize/similarity_cluster_graph +
    70. +
      + +
      +
    71. + + + + + + def similarity_cluster_graph +
    72. +
      + +
      +
    73. + + + + + + end +
    74. +
      + +
      +
    75. + + + + + + +
    76. +
      + +
      +
    77. + + + + + + # GET /courses/1/visualize/similarity_cluster_table +
    78. +
      + +
      +
    79. + + + + + + def similarity_cluster_table +
    80. +
      + +
      +
    81. + + + + + + @clusters = SubmissionCluster.all +
    82. +
      + +
      +
    83. + + + + + + end +
    84. +
      + +
      +
    85. + + + + + + +
    86. +
      + +
      +
    87. + + + + + + # GET /courses/1/visualize/top_similar_submissions +
    88. +
      + +
      +
    89. + + + + + + def top_similar_submissions +
    90. +
      + +
      +
    91. + + + + + + end +
    92. +
      + +
      +
    93. + + + + + + end +
    94. +
      + +
    +
    +
    + + +
    +
    +

    app/helpers/application_helper.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 19 relevant lines. + 0 lines covered and + 19 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + module ApplicationHelper +
    2. +
      + +
      +
    3. + + + + + + # mailer constants +
    4. +
      + +
      +
    5. + + + + + + SSID_MAINTAINER = "The SSID Team" +
    6. +
      + +
      +
    7. + + + + + + SSID_MAINTAINER_EMAIL = "wing.nus@gmail.com" +
    8. +
      + +
      +
    9. + + + + + + EMAIL_DEFAULT_SENDER = "no-reply@ssid.comp.nus.edu.sg" +
    10. +
      + +
      +
    11. + + + + + + +
    12. +
      + +
      +
    13. + + + + + + # copyright +
    14. +
      + +
      +
    15. + + + + + + COPYRIGHT_DATE_RANGE = "2009–2022" +
    16. +
      + +
      +
    17. + + + + + + COPYRIGHT_HOLDER = "Web Information Retrieval and Natural Language Processing Group" +
    18. +
      + +
      +
    19. + + + + + + +
    20. +
      + +
      +
    21. + + + + + + def self.is_application_healthy +
    22. +
      + +
      +
    23. + + + + + + check_memory = `free` +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + # Mem: total used free shared buff/cache available +
    28. +
      + +
      +
    29. + + + + + + if check_memory +
    30. +
      + +
      +
    31. + + + + + + memory_stats = check_memory.split("\n")[1].split("\s") +
    32. +
      + +
      +
    33. + + + + + + puts "Output: #{check_memory}" +
    34. +
      + +
      +
    35. + + + + + + puts "Memory: #{memory_stats}" +
    36. +
      + +
      +
    37. + + + + + + total_memory, available_memory = Integer(memory_stats[1]), Integer(memory_stats[6]) +
    38. +
      + +
      +
    39. + + + + + + return available_memory * 100.0 / total_memory > Rails.application.config.critical_available_memory +
    40. +
      + +
      +
    41. + + + + + + else +
    42. +
      + +
      +
    43. + + + + + + return false +
    44. +
      + +
      +
    45. + + + + + + end +
    46. +
      + +
      +
    47. + + + + + + end +
    48. +
      + +
      +
    49. + + + + + + +
    50. +
      + +
      +
    51. + + + + + + end +
    52. +
      + +
    +
    +
    + + +
    +
    +

    app/helpers/site_helper.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 28 relevant lines. + 0 lines covered and + 28 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + module SiteHelper +
    36. +
      + +
      +
    37. + + + + + + private +
    38. +
      + +
      +
    39. + + + + + + +
    40. +
      + +
      +
    41. + + + + + + def highlight_if_current(url) +
    42. +
      + +
      +
    43. + + + + + + current_url = %Q{http://#{request.env['SERVER_NAME']}#{request.env['REQUEST_URI']}} +
    44. +
      + +
      +
    45. + + + + + + +
    46. +
      + +
      +
    47. + + + + + + if current_url == url +
    48. +
      + +
      +
    49. + + + + + + %Q{ class="highlight"}.html_safe +
    50. +
      + +
      +
    51. + + + + + + else +
    52. +
      + +
      +
    53. + + + + + + "" +
    54. +
      + +
      +
    55. + + + + + + end +
    56. +
      + +
      +
    57. + + + + + + end +
    58. +
      + +
      +
    59. + + + + + + +
    60. +
      + +
      +
    61. + + + + + + def link_to_unless_current(name, url) +
    62. +
      + +
      +
    63. + + + + + + current_url = %Q{http://#{request.env['SERVER_NAME']}#{request.env['REQUEST_URI']}} +
    64. +
      + +
      +
    65. + + + + + + +
    66. +
      + +
      +
    67. + + + + + + ((current_url == url) ? "<span>#{name}</span>" : link_to(name, url)).html_safe +
    68. +
      + +
      +
    69. + + + + + + end +
    70. +
      + +
      +
    71. + + + + + + end +
    72. +
      + +
    +
    +
    + + +
    +
    +

    app/helpers/site_link_renderer.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 24 relevant lines. + 0 lines covered and + 24 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + require 'will_paginate/view_helpers/action_view' +
    2. +
      + +
      +
    3. + + + + + + +
    4. +
      + +
      +
    5. + + + + + + class SiteLinkRenderer < WillPaginate::ActionView::LinkRenderer +
    6. +
      + +
      +
    7. + + + + + + protected +
    8. +
      + +
      +
    9. + + + + + + +
    10. +
      + +
      +
    11. + + + + + + def html_container(html) +
    12. +
      + +
      +
    13. + + + + + + class_name = [container_attributes[:class], "pagination", "justify-content-center"].compact.join(" "); +
    14. +
      + +
      +
    15. + + + + + + tag(:div, tag(:ul, html, :class => "pagination"), :class => class_name) +
    16. +
      + +
      +
    17. + + + + + + end +
    18. +
      + +
      +
    19. + + + + + + +
    20. +
      + +
      +
    21. + + + + + + def page_number(page) +
    22. +
      + +
      +
    23. + + + + + + aria_label = @template.will_paginate_translate(:page_aria_label, :page => page.to_i) { "Page #{page}" } +
    24. +
      + +
      +
    25. + + + + + + list_item_class = (page == current_page) ? "active page-item" : "page-item"; +
    26. +
      + +
      +
    27. + + + + + + tag(:li, link(page, page, :rel => rel_value(page), :"aria-label" => aria_label, :class => "page-link"), :class => list_item_class) +
    28. +
      + +
      +
    29. + + + + + + end +
    30. +
      + +
      +
    31. + + + + + + +
    32. +
      + +
      +
    33. + + + + + + def gap +
    34. +
      + +
      +
    35. + + + + + + tag(:li, link('&hellip;'.html_safe, '#', :class => "page-link"), :class => "page-item disabled") +
    36. +
      + +
      +
    37. + + + + + + end +
    38. +
      + +
      +
    39. + + + + + + +
    40. +
      + +
      +
    41. + + + + + + def previous_or_next_page(page, text, classname) +
    42. +
      + +
      +
    43. + + + + + + link_class = page ? "#{classname} page-item" : "#{classname} page-item disabled" +
    44. +
      + +
      +
    45. + + + + + + if page +
    46. +
      + +
      +
    47. + + + + + + tag(:li, link(text, page, :class => "page-link"), :class => link_class) +
    48. +
      + +
      +
    49. + + + + + + else +
    50. +
      + +
      +
    51. + + + + + + tag(:li, link(text, "#", :class => "page-link", :"tabindex" => "-1"), :class => link_class) +
    52. +
      + +
      +
    53. + + + + + + end +
    54. +
      + +
      +
    55. + + + + + + end +
    56. +
      + +
      +
    57. + + + + + + +
    58. +
      + +
      +
    59. + + + + + + end +
    60. +
      + +
    +
    +
    + + +
    +
    +

    app/helpers/site_modal.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 8 relevant lines. + 0 lines covered and + 8 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + class SiteModal +
    2. +
      + +
      +
    3. + + + + + + def initialize(title, supporting_text, primary_action) +
    4. +
      + +
      +
    5. + + + + + + @title = title +
    6. +
      + +
      +
    7. + + + + + + @supporting_text = supporting_text +
    8. +
      + +
      +
    9. + + + + + + @primary_action = primary_action +
    10. +
      + +
      +
    11. + + + + + + end +
    12. +
      + +
      +
    13. + + + + + + attr_reader :title, :supporting_text, :primary_action +
    14. +
      + +
      +
    15. + + + + + + end +
    16. +
      + +
    +
    +
    + + +
    +
    +

    app/helpers/submission_clusters_helper.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 45 relevant lines. + 0 lines covered and + 45 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + module SubmissionClustersHelper +
    36. +
      + +
      +
    37. + + + + + + LEGEND_SIMILARITY_VALUES = (40..90).step(10).to_a.reverse +
    38. +
      + +
      +
    39. + + + + + + LEGEND_COLORS_FOR_SIMILARITY_VALUES = Hash[ +
    40. +
      + +
      +
    41. + + + + + + LEGEND_SIMILARITY_VALUES.zip( +
    42. +
      + +
      +
    43. + + + + + + %w{#dc3545 #fd7e14 #ffc107 #0d6efd #0dcaf0 #01A64D} +
    44. +
      + +
      +
    45. + + + + + + ) +
    46. +
      + +
      +
    47. + + + + + + ] +
    48. +
      + +
      +
    49. + + + + + + LEGEND_WEIGHTS_FOR_SIMILARITY_VALUES = Hash[ +
    50. +
      + +
      +
    51. + + + + + + LEGEND_SIMILARITY_VALUES.zip( +
    52. +
      + +
      +
    53. + + + + + + %w{5 5 5 5 5 5} +
    54. +
      + +
      +
    55. + + + + + + ) +
    56. +
      + +
      +
    57. + + + + + + ] +
    58. +
      + +
      +
    59. + + + + + + +
    60. +
      + +
      +
    61. + + + + + + private +
    62. +
      + +
      +
    63. + + + + + + +
    64. +
      + +
      +
    65. + + + + + + def self.color_for_similarity(val) +
    66. +
      + +
      +
    67. + + + + + + val = val.to_f +
    68. +
      + +
      +
    69. + + + + + + legend_index = LEGEND_SIMILARITY_VALUES.index { |legend_value| +
    70. +
      + +
      +
    71. + + + + + + val >= legend_value +
    72. +
      + +
      +
    73. + + + + + + } +
    74. +
      + +
      +
    75. + + + + + + legend_index = LEGEND_SIMILARITY_VALUES.size - 1 unless legend_index +
    76. +
      + +
      +
    77. + + + + + + legend_value = LEGEND_SIMILARITY_VALUES[legend_index] +
    78. +
      + +
      +
    79. + + + + + + LEGEND_COLORS_FOR_SIMILARITY_VALUES[legend_value] +
    80. +
      + +
      +
    81. + + + + + + end +
    82. +
      + +
      +
    83. + + + + + + +
    84. +
      + +
      +
    85. + + + + + + def self.weight_for_similarity(val) +
    86. +
      + +
      +
    87. + + + + + + val = val.to_f +
    88. +
      + +
      +
    89. + + + + + + legend_index = LEGEND_SIMILARITY_VALUES.index { |legend_value| +
    90. +
      + +
      +
    91. + + + + + + val >= legend_value +
    92. +
      + +
      +
    93. + + + + + + } +
    94. +
      + +
      +
    95. + + + + + + legend_index = LEGEND_SIMILARITY_VALUES.size - 1 unless legend_index +
    96. +
      + +
      +
    97. + + + + + + legend_value = LEGEND_SIMILARITY_VALUES[legend_index] +
    98. +
      + +
      +
    99. + + + + + + LEGEND_WEIGHTS_FOR_SIMILARITY_VALUES[legend_value] +
    100. +
      + +
      +
    101. + + + + + + end +
    102. +
      + +
      +
    103. + + + + + + end +
    104. +
      + +
    +
    +
    + + +
    +
    +

    app/mailers/application_mailer.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 4 relevant lines. + 0 lines covered and + 4 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + class ApplicationMailer < ActionMailer::Base +
    2. +
      + +
      +
    3. + + + + + + default from: 'from@example.com' +
    4. +
      + +
      +
    5. + + + + + + layout 'mailer' +
    6. +
      + +
      +
    7. + + + + + + end +
    8. +
      + +
    +
    +
    + + +
    +
    +

    app/mailers/user_mailer.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 7 relevant lines. + 0 lines covered and + 7 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + class UserMailer < ApplicationMailer +
    2. +
      + +
      +
    3. + + + + + + default from: ApplicationHelper::EMAIL_DEFAULT_SENDER +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + def admin_approved(user) +
    8. +
      + +
      +
    9. + + + + + + @user = user +
    10. +
      + +
      +
    11. + + + + + + mail(to: @user.email, subject: "[SSID] Your account has been approved.") +
    12. +
      + +
      +
    13. + + + + + + end +
    14. +
      + +
      +
    15. + + + + + + end +
    16. +
      + +
    +
    +
    + + +
    +
    +

    app/models/announcement.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 16 relevant lines. + 0 lines covered and + 16 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class Announcement < ActiveRecord::Base +
    36. +
      + +
      +
    37. + + + + + + belongs_to :announceable, polymorphic: true +
    38. +
      + +
      +
    39. + + + + + + end +
    40. +
      + +
    +
    +
    + + +
    +
    +

    app/models/application_record.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 3 relevant lines. + 0 lines covered and + 3 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + class ApplicationRecord < ActiveRecord::Base +
    2. +
      + +
      +
    3. + + + + + + self.abstract_class = true +
    4. +
      + +
      +
    5. + + + + + + end +
    6. +
      + +
    +
    +
    + + +
    +
    +

    app/models/assignment.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 76 relevant lines. + 0 lines covered and + 76 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class Assignment < ActiveRecord::Base +
    36. +
      + +
      +
    37. + + + + + + has_many :submission_similarities, :dependent => :delete_all +
    38. +
      + +
      +
    39. + + + + + + has_many :submission_cluster_groups, -> { order("cut_off_criterion DESC") }, :dependent => :delete_all +
    40. +
      + +
      +
    41. + + + + + + has_one :submission_similarity_process, :dependent => :delete +
    42. +
      + +
      +
    43. + + + + + + has_many :suspected_plagiarism_cases, -> { where("status = #{SubmissionSimilarity::STATUS_SUSPECTED_AS_PLAGIARISM}") }, class_name: "SubmissionSimilarity" +
    44. +
      + +
      +
    45. + + + + + + has_many :confirmed_or_suspected_plagiarism_cases, -> { where("status = #{SubmissionSimilarity::STATUS_CONFIRMED_AS_PLAGIARISM} OR status = #{SubmissionSimilarity::STATUS_SUSPECTED_AS_PLAGIARISM}") }, class_name: "SubmissionSimilarity" +
    46. +
      + +
      +
    47. + + + + + + has_many :confirmed_plagiarism_cases, -> { where("status = #{SubmissionSimilarity::STATUS_CONFIRMED_AS_PLAGIARISM}") }, class_name: "SubmissionSimilarity" +
    48. +
      + +
      +
    49. + + + + + + has_many :submissions +
    50. +
      + +
      +
    51. + + + + + + belongs_to :course +
    52. +
      + +
      +
    53. + + + + + + attr_accessor :used_fingerprints +
    54. +
      + +
      +
    55. + + + + + + +
    56. +
      + +
      +
    57. + + + + + + validates :title, :language, :min_match_length, :ngram_size, presence: true +
    58. +
      + +
      +
    59. + + + + + + validates_numericality_of :min_match_length, only_integer: true, greater_than: 0 +
    60. +
      + +
      +
    61. + + + + + + validates_numericality_of :ngram_size, only_integer: true, greater_than: 0 +
    62. +
      + +
      +
    63. + + + + + + +
    64. +
      + +
      +
    65. + + + + + + LANGUAGES = { +
    66. +
      + +
      +
    67. + + + + + + java: "Java", +
    68. +
      + +
      +
    69. + + + + + + c: "C", +
    70. +
      + +
      +
    71. + + + + + + cpp: "C++", +
    72. +
      + +
      +
    73. + + + + + + python3: "Python3", +
    74. +
      + +
      +
    75. + + + + + + scala: "Scala", +
    76. +
      + +
      +
    77. + + + + + + matlab: "Matlab", +
    78. +
      + +
      +
    79. + + + + + + r: "R", +
    80. +
      + +
      +
    81. + + + + + + ocaml: "Ocaml" +
    82. +
      + +
      +
    83. + + + + + + } +
    84. +
      + +
      +
    85. + + + + + + +
    86. +
      + +
      +
    87. + + + + + + PRETTIFY_LANGUAGES = { +
    88. +
      + +
      +
    89. + + + + + + java: "java", +
    90. +
      + +
      +
    91. + + + + + + c: "c", +
    92. +
      + +
      +
    93. + + + + + + cpp: "cpp", +
    94. +
      + +
      +
    95. + + + + + + python3: "py", +
    96. +
      + +
      +
    97. + + + + + + scala: "scala", +
    98. +
      + +
      +
    99. + + + + + + matlab: "matlab", +
    100. +
      + +
      +
    101. + + + + + + r: "r", +
    102. +
      + +
      +
    103. + + + + + + ocaml: "ocaml" +
    104. +
      + +
      +
    105. + + + + + + } +
    106. +
      + +
      +
    107. + + + + + + +
    108. +
      + +
      +
    109. + + + + + + def self.options_for_languages +
    110. +
      + +
      +
    111. + + + + + + LANGUAGES.to_a.collect { |pair| pair.reverse } +
    112. +
      + +
      +
    113. + + + + + + end +
    114. +
      + +
      +
    115. + + + + + + +
    116. +
      + +
      +
    117. + + + + + + def prettify_js_lang +
    118. +
      + +
      +
    119. + + + + + + "lang-#{PRETTIFY_LANGUAGES[self.language]}" +
    120. +
      + +
      +
    121. + + + + + + end +
    122. +
      + +
      +
    123. + + + + + + +
    124. +
      + +
      +
    125. + + + + + + def language_string +
    126. +
      + +
      +
    127. + + + + + + LANGUAGES[language.intern] +
    128. +
      + +
      +
    129. + + + + + + end +
    130. +
      + +
      +
    131. + + + + + + +
    132. +
      + +
      +
    133. + + + + + + def cluster_students +
    134. +
      + +
      +
    135. + + + + + + self.submission_cluster_groups.collect { |g| +
    136. +
      + +
      +
    137. + + + + + + g.clusters.collect { |c| +
    138. +
      + +
      +
    139. + + + + + + c.submissions.collect { |s| +
    140. +
      + +
      +
    141. + + + + + + s.student +
    142. +
      + +
      +
    143. + + + + + + } +
    144. +
      + +
      +
    145. + + + + + + } +
    146. +
      + +
      +
    147. + + + + + + }.flatten.uniq.sort +
    148. +
      + +
      +
    149. + + + + + + end +
    150. +
      + +
      +
    151. + + + + + + +
    152. +
      + +
      +
    153. + + + + + + def submission_clusters +
    154. +
      + +
      +
    155. + + + + + + self.submission_cluster_groups.collect { |g| +
    156. +
      + +
      +
    157. + + + + + + g.clusters +
    158. +
      + +
      +
    159. + + + + + + }.flatten +
    160. +
      + +
      +
    161. + + + + + + end +
    162. +
      + +
      +
    163. + + + + + + +
    164. +
      + +
      +
    165. + + + + + + def submission_similarities_for_student(student, num_display) +
    166. +
      + +
      +
    167. + + + + + + the_submission = self.submissions.find_by_student_id(student.id) +
    168. +
      + +
      +
    169. + + + + + + +
    170. +
      + +
      +
    171. + + + + + + if the_submission +
    172. +
      + +
      +
    173. + + + + + + self.submission_similarities.where(["submission1_id = ? or submission2_id = ?", the_submission.id, the_submission.id]).order('assignment_id, similarity DESC').limit(num_display) +
    174. +
      + +
      +
    175. + + + + + + end +
    176. +
      + +
      +
    177. + + + + + + end +
    178. +
      + +
      +
    179. + + + + + + end +
    180. +
      + +
    +
    +
    + + +
    +
    +

    app/models/course.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 118 relevant lines. + 0 lines covered and + 118 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class TimeValidator < ActiveModel::EachValidator +
    36. +
      + +
      +
    37. + + + + + + def validate_each(record, attribute, value) +
    38. +
      + +
      +
    39. + + + + + + if Time.parse(value.to_s).to_i <= Time.now.to_i +
    40. +
      + +
      +
    41. + + + + + + record.errors[attribute] << (options[:message] || "should be in the future") +
    42. +
      + +
      +
    43. + + + + + + end +
    44. +
      + +
      +
    45. + + + + + + end +
    46. +
      + +
      +
    47. + + + + + + end +
    48. +
      + +
      +
    49. + + + + + + +
    50. +
      + +
      +
    51. + + + + + + class Course < ActiveRecord::Base +
    52. +
      + +
      +
    53. + + + + + + has_many :announcements, -> { order "updated_at DESC" }, :as => :announceable +
    54. +
      + +
      +
    55. + + + + + + has_many :assignments, :dependent => :delete_all +
    56. +
      + +
      +
    57. + + + + + + has_many :memberships, class_name: "UserCourseMembership", :dependent => :delete_all +
    58. +
      + +
      +
    59. + + + + + + has_many :users, :through => :memberships +
    60. +
      + +
      +
    61. + + + + + + has_many :student_memberships, -> { where role: UserCourseMembership::ROLE_STUDENT }, class_name: "UserCourseMembership" +
    62. +
      + +
      +
    63. + + + + + + has_many :staff_memberships, -> { where role: UserCourseMembership::ROLE_TEACHING_STAFF }, class_name: "UserCourseMembership" +
    64. +
      + +
      +
    65. + + + + + + has_many :teaching_assistant_memberships, -> { where role: UserCourseMembership::ROLE_TEACHING_ASSISTANT }, class_name: "UserCourseMembership" +
    66. +
      + +
      +
    67. + + + + + + has_many :guest_memberships, -> { where role: UserCourseMembership::ROLE_GUEST }, class_name: "UserCourseMembership" +
    68. +
      + +
      +
    69. + + + + + + has_many :guest_users_detail, :dependent => :delete_all, class_name: "GuestUsersDetail" +
    70. +
      + +
      +
    71. + + + + + + +
    72. +
      + +
      +
    73. + + + + + + validates_presence_of :code +
    74. +
      + +
      +
    75. + + + + + + validates_presence_of :name +
    76. +
      + +
      +
    77. + + + + + + validates :expiry, time: true +
    78. +
      + +
      +
    79. + + + + + + validates :code, uniqueness: { :scope => [ :academic_year, :semester ] } +
    80. +
      + +
      +
    81. + + + + + + before_save :upcase_code +
    82. +
      + +
      +
    83. + + + + + + +
    84. +
      + +
      +
    85. + + + + + + def code_and_name +
    86. +
      + +
      +
    87. + + + + + + [self.code, self.name].join(" ") +
    88. +
      + +
      +
    89. + + + + + + end +
    90. +
      + +
      +
    91. + + + + + + +
    92. +
      + +
      +
    93. + + + + + + def students +
    94. +
      + +
      +
    95. + + + + + + self.student_memberships.collect { |m| m.user } +
    96. +
      + +
      +
    97. + + + + + + end +
    98. +
      + +
      +
    99. + + + + + + +
    100. +
      + +
      +
    101. + + + + + + def staff +
    102. +
      + +
      +
    103. + + + + + + self.staff_memberships.collect { |m| m.user } +
    104. +
      + +
      +
    105. + + + + + + end +
    106. +
      + +
      +
    107. + + + + + + +
    108. +
      + +
      +
    109. + + + + + + def teaching_assistants +
    110. +
      + +
      +
    111. + + + + + + self.teaching_assistant_memberships.collect { |m| m.user } +
    112. +
      + +
      +
    113. + + + + + + end +
    114. +
      + +
      +
    115. + + + + + + +
    116. +
      + +
      +
    117. + + + + + + def guests +
    118. +
      + +
      +
    119. + + + + + + self.guest_memberships.collect { |m| m.user } +
    120. +
      + +
      +
    121. + + + + + + end +
    122. +
      + +
      +
    123. + + + + + + +
    124. +
      + +
      +
    125. + + + + + + def submission_cluster_groups +
    126. +
      + +
      +
    127. + + + + + + self.assignments.collect { |assignment| +
    128. +
      + +
      +
    129. + + + + + + assignment.submission_cluster_groups +
    130. +
      + +
      +
    131. + + + + + + }.flatten +
    132. +
      + +
      +
    133. + + + + + + end +
    134. +
      + +
      +
    135. + + + + + + +
    136. +
      + +
      +
    137. + + + + + + def submission_clusters +
    138. +
      + +
      +
    139. + + + + + + self.assignments.collect { |assignment| +
    140. +
      + +
      +
    141. + + + + + + assignment.submission_clusters +
    142. +
      + +
      +
    143. + + + + + + }.flatten +
    144. +
      + +
      +
    145. + + + + + + end +
    146. +
      + +
      +
    147. + + + + + + +
    148. +
      + +
      +
    149. + + + + + + def empty_assignments +
    150. +
      + +
      +
    151. + + + + + + self.assignments.reject { |a| +
    152. +
      + +
      +
    153. + + + + + + !a.submission_similarity_process.nil? +
    154. +
      + +
      +
    155. + + + + + + } +
    156. +
      + +
      +
    157. + + + + + + end +
    158. +
      + +
      +
    159. + + + + + + +
    160. +
      + +
      +
    161. + + + + + + def processing_assignments +
    162. +
      + +
      +
    163. + + + + + + self.assignments.reject { |a| +
    164. +
      + +
      +
    165. + + + + + + a.submission_similarity_process.nil? or +
    166. +
      + +
      +
    167. + + + + + + a.submission_similarity_process.status != SubmissionSimilarityProcess::STATUS_RUNNING +
    168. +
      + +
      +
    169. + + + + + + } +
    170. +
      + +
      +
    171. + + + + + + end +
    172. +
      + +
      +
    173. + + + + + + +
    174. +
      + +
      +
    175. + + + + + + def processed_assignments +
    176. +
      + +
      +
    177. + + + + + + self.assignments.reject { |a| +
    178. +
      + +
      +
    179. + + + + + + a.submission_similarity_process.nil? or +
    180. +
      + +
      +
    181. + + + + + + a.submission_similarity_process.status != SubmissionSimilarityProcess::STATUS_COMPLETED +
    182. +
      + +
      +
    183. + + + + + + } +
    184. +
      + +
      +
    185. + + + + + + end +
    186. +
      + +
      +
    187. + + + + + + +
    188. +
      + +
      +
    189. + + + + + + def erroneous_assignments +
    190. +
      + +
      +
    191. + + + + + + self.assignments.reject { |a| +
    192. +
      + +
      +
    193. + + + + + + a.submission_similarity_process.nil? or +
    194. +
      + +
      +
    195. + + + + + + a.submission_similarity_process.status != SubmissionSimilarityProcess::STATUS_ERRONEOUS +
    196. +
      + +
      +
    197. + + + + + + } +
    198. +
      + +
      +
    199. + + + + + + end +
    200. +
      + +
      +
    201. + + + + + + +
    202. +
      + +
      +
    203. + + + + + + def membership_for_user(user) +
    204. +
      + +
      +
    205. + + + + + + UserCourseMembership.where( course_id: self.id, +
    206. +
      + +
      +
    207. + + + + + + user_id: user.id).first +
    208. +
      + +
      +
    209. + + + + + + end +
    210. +
      + +
      +
    211. + + + + + + +
    212. +
      + +
      +
    213. + + + + + + def guest_user_finder(user) +
    214. +
      + +
      +
    215. + + + + + + GuestUsersDetail.where( course_id: self.id, +
    216. +
      + +
      +
    217. + + + + + + user_id: user.id).first +
    218. +
      + +
      +
    219. + + + + + + end +
    220. +
      + +
      +
    221. + + + + + + +
    222. +
      + +
      +
    223. + + + + + + def role_string_for_user(user) +
    224. +
      + +
      +
    225. + + + + + + user.is_admin ? "Administrator" : self.membership_for_user(user).role_string +
    226. +
      + +
      +
    227. + + + + + + end +
    228. +
      + +
      +
    229. + + + + + + +
    230. +
      + +
      +
    231. + + + + + + def all_submission_similarity_cluster_groups +
    232. +
      + +
      +
    233. + + + + + + self.assignments.collect { |a| +
    234. +
      + +
      +
    235. + + + + + + a.submission_cluster_groups +
    236. +
      + +
      +
    237. + + + + + + }.flatten +
    238. +
      + +
      +
    239. + + + + + + end +
    240. +
      + +
      +
    241. + + + + + + +
    242. +
      + +
      +
    243. + + + + + + def cluster_students +
    244. +
      + +
      +
    245. + + + + + + self.assignments.collect { |a| +
    246. +
      + +
      +
    247. + + + + + + a.cluster_students +
    248. +
      + +
      +
    249. + + + + + + }.flatten.uniq.sort +
    250. +
      + +
      +
    251. + + + + + + end +
    252. +
      + +
      +
    253. + + + + + + +
    254. +
      + +
      +
    255. + + + + + + def self.options_for_academic_year +
    256. +
      + +
      +
    257. + + + + + + starting_year = Time.now.in_time_zone.year - 1 +
    258. +
      + +
      +
    259. + + + + + + (starting_year..(starting_year+5)).to_a.collect { |y| +
    260. +
      + +
      +
    261. + + + + + + "#{y}/#{y+1}" +
    262. +
      + +
      +
    263. + + + + + + } +
    264. +
      + +
      +
    265. + + + + + + end +
    266. +
      + +
      +
    267. + + + + + + +
    268. +
      + +
      +
    269. + + + + + + def find_guest_user_details(user_id) +
    270. +
      + +
      +
    271. + + + + + + GuestUsersDetail.where("user_id = ? AND course_id = ? ", user_id, self.id) +
    272. +
      + +
      +
    273. + + + + + + end +
    274. +
      + +
      +
    275. + + + + + + +
    276. +
      + +
      +
    277. + + + + + + private +
    278. +
      + +
      +
    279. + + + + + + +
    280. +
      + +
      +
    281. + + + + + + def upcase_code +
    282. +
      + +
      +
    283. + + + + + + self.code = self.code.upcase +
    284. +
      + +
      +
    285. + + + + + + end +
    286. +
      + +
      +
    287. + + + + + + end +
    288. +
      + +
    +
    +
    + + +
    +
    +

    app/models/guest_users_detail.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 4 relevant lines. + 0 lines covered and + 4 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + class GuestUsersDetail < ApplicationRecord +
    2. +
      + +
      +
    3. + + + + + + belongs_to :course +
    4. +
      + +
      +
    5. + + + + + + belongs_to :user +
    6. +
      + +
      +
    7. + + + + + + end +
    8. +
      + +
    +
    +
    + + +
    +
    +

    app/models/submission.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 44 relevant lines. + 0 lines covered and + 44 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class Submission < ActiveRecord::Base +
    36. +
      + +
      +
    37. + + + + + + has_many :similarities, class_name: "SubmissionSimilarity" +
    38. +
      + +
      +
    39. + + + + + + has_many :cluster_memberships , class_name: "SubmissionClusterMembership" +
    40. +
      + +
      +
    41. + + + + + + has_many :logs, :dependent => :delete_all, class_name: "SubmissionLog" +
    42. +
      + +
      +
    43. + + + + + + belongs_to :student, class_name: "User" +
    44. +
      + +
      +
    45. + + + + + + belongs_to :assignment +
    46. +
      + +
      +
    47. + + + + + + +
    48. +
      + +
      +
    49. + + + + + + serialize :lines, Array +
    50. +
      + +
      +
    51. + + + + + + +
    52. +
      + +
      +
    53. + + + + + + before_save :check_lines +
    54. +
      + +
      +
    55. + + + + + + +
    56. +
      + +
      +
    57. + + + + + + def course +
    58. +
      + +
      +
    59. + + + + + + self.assignment.course +
    60. +
      + +
      +
    61. + + + + + + end +
    62. +
      + +
      +
    63. + + + + + + +
    64. +
      + +
      +
    65. + + + + + + # Make sure that lines is an array +
    66. +
      + +
      +
    67. + + + + + + def check_lines +
    68. +
      + +
      +
    69. + + + + + + attr = self.read_attribute("lines") +
    70. +
      + +
      +
    71. + + + + + + unless attr.class == Array +
    72. +
      + +
      +
    73. + + + + + + attr = YAML::load(attr) +
    74. +
      + +
      +
    75. + + + + + + raise unless attr.class == Array +
    76. +
      + +
      +
    77. + + + + + + write_attribute("lines", attr) +
    78. +
      + +
      +
    79. + + + + + + end +
    80. +
      + +
      +
    81. + + + + + + end +
    82. +
      + +
      +
    83. + + + + + + +
    84. +
      + +
      +
    85. + + + + + + # Auto-(de)serialization does not work with the format written by snakeyaml java library, +
    86. +
      + +
      +
    87. + + + + + + # So we force (de)serialize by overriding accessor methods +
    88. +
      + +
      +
    89. + + + + + + def lines +
    90. +
      + +
      +
    91. + + + + + + attr = self.read_attribute("lines") +
    92. +
      + +
      +
    93. + + + + + + unless attr.class == Array +
    94. +
      + +
      +
    95. + + + + + + attr = YAML::load(attr) +
    96. +
      + +
      +
    97. + + + + + + raise unless attr.class == Array +
    98. +
      + +
      +
    99. + + + + + + end +
    100. +
      + +
      +
    101. + + + + + + attr +
    102. +
      + +
      +
    103. + + + + + + end +
    104. +
      + +
      +
    105. + + + + + + +
    106. +
      + +
      +
    107. + + + + + + def student_name +
    108. +
      + +
      +
    109. + + + + + + self.student.name +
    110. +
      + +
      +
    111. + + + + + + end +
    112. +
      + +
      +
    113. + + + + + + end +
    114. +
      + +
    +
    +
    + + +
    +
    +

    app/models/submission_cluster.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 29 relevant lines. + 0 lines covered and + 29 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionCluster < ActiveRecord::Base +
    36. +
      + +
      +
    37. + + + + + + belongs_to :submission_cluster_group +
    38. +
      + +
      +
    39. + + + + + + has_many :memberships, class_name: "SubmissionClusterMembership", :dependent => :delete_all +
    40. +
      + +
      +
    41. + + + + + + has_many :submissions, -> { distinct }, :through => :memberships +
    42. +
      + +
      +
    43. + + + + + + +
    44. +
      + +
      +
    45. + + + + + + def submission_student_ids +
    46. +
      + +
      +
    47. + + + + + + self.submissions.collect { |s| +
    48. +
      + +
      +
    49. + + + + + + s.student.name +
    50. +
      + +
      +
    51. + + + + + + } +
    52. +
      + +
      +
    53. + + + + + + end +
    54. +
      + +
      +
    55. + + + + + + +
    56. +
      + +
      +
    57. + + + + + + def assignment +
    58. +
      + +
      +
    59. + + + + + + self.submission_cluster_group.assignment +
    60. +
      + +
      +
    61. + + + + + + end +
    62. +
      + +
      +
    63. + + + + + + +
    64. +
      + +
      +
    65. + + + + + + def course +
    66. +
      + +
      +
    67. + + + + + + self.assignment.course +
    68. +
      + +
      +
    69. + + + + + + end +
    70. +
      + +
      +
    71. + + + + + + end +
    72. +
      + +
    +
    +
    + + +
    +
    +

    app/models/submission_cluster_group.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 36 relevant lines. + 0 lines covered and + 36 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionClusterGroup < ActiveRecord::Base +
    36. +
      + +
      +
    37. + + + + + + # SubmissionClusterGroup#cut_off_criterion_type Constants +
    38. +
      + +
      +
    39. + + + + + + TYPE_CONFIRMED_OR_SUSPECTED_PLAGIARISM_CRITERION = 0 +
    40. +
      + +
      +
    41. + + + + + + TYPE_CONFIRMED_PLAGIARISM_CRITERION = 1 +
    42. +
      + +
      +
    43. + + + + + + TYPE_USER_DEFINED_CRITERION = 2 +
    44. +
      + +
      +
    45. + + + + + + TYPE_STRINGS = [ +
    46. +
      + +
      +
    47. + + + + + + "Confirmed or Suspected Plagiarism Cases", +
    48. +
      + +
      +
    49. + + + + + + "Confirmed Plagiarism Cases", +
    50. +
      + +
      +
    51. + + + + + + "User-Defined Value" +
    52. +
      + +
      +
    53. + + + + + + ] +
    54. +
      + +
      +
    55. + + + + + + +
    56. +
      + +
      +
    57. + + + + + + # SubmissionClusterGroup#description Constants +
    58. +
      + +
      +
    59. + + + + + + DESCRIPTIONS = { +
    60. +
      + +
      +
    61. + + + + + + TYPE_CONFIRMED_OR_SUSPECTED_PLAGIARISM_CRITERION => "Based on cases confirmed or suspected as plagiarism", +
    62. +
      + +
      +
    63. + + + + + + TYPE_CONFIRMED_PLAGIARISM_CRITERION => "Based on cases confirmed as plagiarism", +
    64. +
      + +
      +
    65. + + + + + + TYPE_USER_DEFINED_CRITERION => "Based on user-defined cut-off criterion" +
    66. +
      + +
      +
    67. + + + + + + } +
    68. +
      + +
      +
    69. + + + + + + +
    70. +
      + +
      +
    71. + + + + + + has_many :clusters, class_name: "SubmissionCluster", :dependent => :delete_all +
    72. +
      + +
      +
    73. + + + + + + belongs_to :assignment +
    74. +
      + +
      +
    75. + + + + + + +
    76. +
      + +
      +
    77. + + + + + + validates_presence_of :assignment_id +
    78. +
      + +
      +
    79. + + + + + + validates_numericality_of :cut_off_criterion, allow_nil: false, allow_blank: false, greater_than: 0, less_than_or_equal_to: 100 +
    80. +
      + +
      +
    81. + + + + + + validates_uniqueness_of :cut_off_criterion, scope: [:assignment_id, :cut_off_criterion], :message => "has already been created" +
    82. +
      + +
      +
    83. + + + + + + +
    84. +
      + +
      +
    85. + + + + + + def cluster_ids +
    86. +
      + +
      +
    87. + + + + + + self.clusters.collect { |c| c.id } +
    88. +
      + +
      +
    89. + + + + + + end +
    90. +
      + +
      +
    91. + + + + + + end +
    92. +
      + +
    +
    +
    + + +
    +
    +

    app/models/submission_cluster_membership.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 17 relevant lines. + 0 lines covered and + 17 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionClusterMembership < ActiveRecord::Base +
    36. +
      + +
      +
    37. + + + + + + belongs_to :submission_cluster +
    38. +
      + +
      +
    39. + + + + + + belongs_to :submission +
    40. +
      + +
      +
    41. + + + + + + end +
    42. +
      + +
    +
    +
    + + +
    +
    +

    app/models/submission_log.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 33 relevant lines. + 0 lines covered and + 33 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionLog < ActiveRecord::Base +
    36. +
      + +
      +
    37. + + + + + + # SubmissionLog#log_type Constants +
    38. +
      + +
      +
    39. + + + + + + TYPE_PAIR_SUSPECT_AS_PLAGIARISM = 0 +
    40. +
      + +
      +
    41. + + + + + + TYPE_PAIR_CONFIRM_AS_PLAGIARISM = 1 +
    42. +
      + +
      +
    43. + + + + + + TYPE_PAIR_UNMARK_AS_PLAGIARISM = 2 +
    44. +
      + +
      +
    45. + + + + + + TYPE_STUDENT_MARK_AS_GUILTY = 3 +
    46. +
      + +
      +
    47. + + + + + + TYPE_STUDENT_MARK_AS_NOT_GUILTY = 4 +
    48. +
      + +
      +
    49. + + + + + + TYPE_TEMPLATE_STRINGS = [ +
    50. +
      + +
      +
    51. + + + + + + "Suspected as plagiarism with submission by ", +
    52. +
      + +
      +
    53. + + + + + + "Confirmed as plagiarism with submission by ", +
    54. +
      + +
      +
    55. + + + + + + "Unmarked as plagiarism with submission by ", +
    56. +
      + +
      +
    57. + + + + + + "Marked as guilty of plagiarism of submission by ", +
    58. +
      + +
      +
    59. + + + + + + "Unmarked as guilty of plagiarism of submission by " +
    60. +
      + +
      +
    61. + + + + + + ] +
    62. +
      + +
      +
    63. + + + + + + +
    64. +
      + +
      +
    65. + + + + + + belongs_to :marker, class_name: "User" +
    66. +
      + +
      +
    67. + + + + + + belongs_to :submission +
    68. +
      + +
      +
    69. + + + + + + belongs_to :submission_similarity +
    70. +
      + +
      +
    71. + + + + + + +
    72. +
      + +
      +
    73. + + + + + + def log_string +
    74. +
      + +
      +
    75. + + + + + + TYPE_TEMPLATE_STRINGS[self.log_type] + self.submission_similarity.other_student(self.submission.student).name +
    76. +
      + +
      +
    77. + + + + + + end +
    78. +
      + +
      +
    79. + + + + + + end +
    80. +
      + +
    +
    +
    + + +
    +
    +

    app/models/submission_similarity.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 49 relevant lines. + 0 lines covered and + 49 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionSimilarity < ActiveRecord::Base +
    36. +
      + +
      +
    37. + + + + + + # SubmissionSimilarity#status Constants +
    38. +
      + +
      +
    39. + + + + + + STATUS_NOT_PLAGIARISM = 0 +
    40. +
      + +
      +
    41. + + + + + + STATUS_CONFIRMED_AS_PLAGIARISM = 1 +
    42. +
      + +
      +
    43. + + + + + + STATUS_SUSPECTED_AS_PLAGIARISM = 2 +
    44. +
      + +
      +
    45. + + + + + + STATUS_STRINGS = [ +
    46. +
      + +
      +
    47. + + + + + + "", +
    48. +
      + +
      +
    49. + + + + + + "Confirmed Plagiarism", +
    50. +
      + +
      +
    51. + + + + + + "Suspected of Plagiarism" +
    52. +
      + +
      +
    53. + + + + + + ] +
    54. +
      + +
      +
    55. + + + + + + +
    56. +
      + +
      +
    57. + + + + + + belongs_to :assignment +
    58. +
      + +
      +
    59. + + + + + + belongs_to :submission1, class_name: "Submission" +
    60. +
      + +
      +
    61. + + + + + + belongs_to :submission2, class_name: "Submission" +
    62. +
      + +
      +
    63. + + + + + + has_one :course, :through => :assignment +
    64. +
      + +
      +
    65. + + + + + + has_many :mappings, :dependent => :delete_all, :class_name => "SubmissionSimilarityMapping" +
    66. +
      + +
      +
    67. + + + + + + +
    68. +
      + +
      +
    69. + + + + + + def status_string +
    70. +
      + +
      +
    71. + + + + + + STATUS_STRINGS[self.status] +
    72. +
      + +
      +
    73. + + + + + + end +
    74. +
      + +
      +
    75. + + + + + + +
    76. +
      + +
      +
    77. + + + + + + def average_similarity +
    78. +
      + +
      +
    79. + + + + + + 0.5 * (self.similarity_1_to_2 + self.similarity_2_to_1) +
    80. +
      + +
      +
    81. + + + + + + end +
    82. +
      + +
      +
    83. + + + + + + +
    84. +
      + +
      +
    85. + + + + + + def student1 +
    86. +
      + +
      +
    87. + + + + + + self.submission1.student +
    88. +
      + +
      +
    89. + + + + + + end +
    90. +
      + +
      +
    91. + + + + + + +
    92. +
      + +
      +
    93. + + + + + + def student2 +
    94. +
      + +
      +
    95. + + + + + + self.submission2.student +
    96. +
      + +
      +
    97. + + + + + + end +
    98. +
      + +
      +
    99. + + + + + + +
    100. +
      + +
      +
    101. + + + + + + def other_student(student) +
    102. +
      + +
      +
    103. + + + + + + (student == submission1.student) ? submission2.student : submission1.student +
    104. +
      + +
      +
    105. + + + + + + end +
    106. +
      + +
      +
    107. + + + + + + +
    108. +
      + +
      +
    109. + + + + + + def other_submission(student) +
    110. +
      + +
      +
    111. + + + + + + (student == submission1.student) ? submission2 : submission1 +
    112. +
      + +
      +
    113. + + + + + + end +
    114. +
      + +
      +
    115. + + + + + + +
    116. +
      + +
      +
    117. + + + + + + def similarity_mappings +
    118. +
      + +
      +
    119. + + + + + + SubmissionSimilarityMapping.where("submission_similarity_id = ? AND statement_count > 0", self.id) +
    120. +
      + +
      +
    121. + + + + + + end +
    122. +
      + +
      +
    123. + + + + + + end +
    124. +
      + +
    +
    +
    + + +
    +
    +

    app/models/submission_similarity_mapping.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 32 relevant lines. + 0 lines covered and + 32 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionSimilarityMapping < ActiveRecord::Base +
    36. +
      + +
      +
    37. + + + + + + belongs_to :submission_similarity +
    38. +
      + +
      +
    39. + + + + + + has_many :submission_similarity_skeleton_mappings, :dependent => :delete_all, class_name: "SubmissionSimilaritySkeletonMapping" +
    40. +
      + +
      +
    41. + + + + + + +
    42. +
      + +
      +
    43. + + + + + + def line_range1 +
    44. +
      + +
      +
    45. + + + + + + (self.start_line1)..(self.end_line1) +
    46. +
      + +
      +
    47. + + + + + + end +
    48. +
      + +
      +
    49. + + + + + + +
    50. +
      + +
      +
    51. + + + + + + def line_range2 +
    52. +
      + +
      +
    53. + + + + + + (self.start_line2)..(self.end_line2) +
    54. +
      + +
      +
    55. + + + + + + end +
    56. +
      + +
      +
    57. + + + + + + +
    58. +
      + +
      +
    59. + + + + + + def line_range1_string +
    60. +
      + +
      +
    61. + + + + + + "#{self.start_line1 + 1} &mdash; #{self.end_line1 + 1}".html_safe +
    62. +
      + +
      +
    63. + + + + + + end +
    64. +
      + +
      +
    65. + + + + + + +
    66. +
      + +
      +
    67. + + + + + + def line_range2_string +
    68. +
      + +
      +
    69. + + + + + + "#{self.start_line2 + 1} &mdash; #{self.end_line2 + 1}".html_safe +
    70. +
      + +
      +
    71. + + + + + + end +
    72. +
      + +
      +
    73. + + + + + + +
    74. +
      + +
      +
    75. + + + + + + def line_ranges_html_value +
    76. +
      + +
      +
    77. + + + + + + "#{self.start_line1}_#{self.end_line1}_#{self.start_line2}_#{self.end_line2}" +
    78. +
      + +
      +
    79. + + + + + + end +
    80. +
      + +
      +
    81. + + + + + + end +
    82. +
      + +
    +
    +
    + + +
    +
    +

    app/models/submission_similarity_process.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 21 relevant lines. + 0 lines covered and + 21 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class SubmissionSimilarityProcess < ActiveRecord::Base +
    36. +
      + +
      +
    37. + + + + + + # SubmissionSimilarityProcess#status Constants +
    38. +
      + +
      +
    39. + + + + + + STATUS_ERRONEOUS = 0 +
    40. +
      + +
      +
    41. + + + + + + STATUS_RUNNING = 1 +
    42. +
      + +
      +
    43. + + + + + + STATUS_COMPLETED = 2 +
    44. +
      + +
      +
    45. + + + + + + STATUS_WAITING = 3 +
    46. +
      + +
      +
    47. + + + + + + STATUS_STRINGS = %w{erroneous running completed} +
    48. +
      + +
      +
    49. + + + + + + +
    50. +
      + +
      +
    51. + + + + + + belongs_to :assignment +
    52. +
      + +
      +
    53. + + + + + + end +
    54. +
      + +
    +
    +
    + + +
    +
    +

    app/models/submission_similarity_skeleton_mapping.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 3 relevant lines. + 0 lines covered and + 3 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + class SubmissionSimilaritySkeletonMapping < ActiveRecord::Base +
    2. +
      + +
      +
    3. + + + + + + belongs_to :submission_similarity_mapping +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + end +
    8. +
      + +
    +
    +
    + + +
    +
    +

    app/models/user.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 69 relevant lines. + 0 lines covered and + 69 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class User < ActiveRecord::Base +
    36. +
      + +
      +
    37. + + + + + + # Include default devise modules. Others available are: +
    38. +
      + +
      +
    39. + + + + + + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable +
    40. +
      + +
      +
    41. + + + + + + devise :database_authenticatable, :registerable, +
    42. +
      + +
      +
    43. + + + + + + :recoverable, :rememberable, :validatable, :confirmable, +
    44. +
      + +
      +
    45. + + + + + + :omniauthable, :omniauth_providers => [:google_oauth2] +
    46. +
      + +
      +
    47. + + + + + + MIN_PASSWORD_LENGTH = 8 +
    48. +
      + +
      +
    49. + + + + + + +
    50. +
      + +
      +
    51. + + + + + + has_many :memberships , class_name: "UserCourseMembership", :dependent => :delete_all +
    52. +
      + +
      +
    53. + + + + + + has_many :guest_users_detail, class_name: "GuestUsersDetail", :dependent => :delete_all +
    54. +
      + +
      +
    55. + + + + + + has_many :courses, -> { distinct }, :through => :memberships +
    56. +
      + +
      +
    57. + + + + + + has_many :assignments, -> { distinct }, :through => :courses +
    58. +
      + +
      +
    59. + + + + + + has_many :submissions, foreign_key: "student_id" +
    60. +
      + +
      +
    61. + + + + + + +
    62. +
      + +
      +
    63. + + + + + + validates :name, presence: true +
    64. +
      + +
      +
    65. + + + + + + validates :name, uniqueness: true +
    66. +
      + +
      +
    67. + + + + + + validates :email, presence: true, uniqueness: true, format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i } +
    68. +
      + +
      +
    69. + + + + + + validate :password_complexity +
    70. +
      + +
      +
    71. + + + + + + +
    72. +
      + +
      +
    73. + + + + + + before_destroy :ensure_an_admin_remains +
    74. +
      + +
      +
    75. + + + + + + +
    76. +
      + +
      +
    77. + + + + + + def is_some_staff? +
    78. +
      + +
      +
    79. + + + + + + self.courses.any? { |c| c.membership_for_user(self).role == UserCourseMembership::ROLE_TEACHING_STAFF } +
    80. +
      + +
      +
    81. + + + + + + end +
    82. +
      + +
      +
    83. + + + + + + +
    84. +
      + +
      +
    85. + + + + + + def is_staff_or_ta? +
    86. +
      + +
      +
    87. + + + + + + UserCourseMembership.where(["user_id = ? AND role IN (?, ?)", self.id, 0, 1]) +
    88. +
      + +
      +
    89. + + + + + + end +
    90. +
      + +
      +
    91. + + + + + + +
    92. +
      + +
      +
    93. + + + + + + def full_name +
    94. +
      + +
      +
    95. + + + + + + the_full_name = self.read_attribute(:full_name) || "" +
    96. +
      + +
      +
    97. + + + + + + the_full_name.strip.empty? ? nil : the_full_name +
    98. +
      + +
      +
    99. + + + + + + end +
    100. +
      + +
      +
    101. + + + + + + +
    102. +
      + +
      +
    103. + + + + + + def active_for_authentication? +
    104. +
      + +
      +
    105. + + + + + + super && self.is_admin_approved? +
    106. +
      + +
      +
    107. + + + + + + end +
    108. +
      + +
      +
    109. + + + + + + +
    110. +
      + +
      +
    111. + + + + + + def inactive_message +
    112. +
      + +
      +
    113. + + + + + + if !self.is_admin_approved? +
    114. +
      + +
      +
    115. + + + + + + :not_approved +
    116. +
      + +
      +
    117. + + + + + + else +
    118. +
      + +
      +
    119. + + + + + + super # Use whatever other message +
    120. +
      + +
      +
    121. + + + + + + end +
    122. +
      + +
      +
    123. + + + + + + end +
    124. +
      + +
      +
    125. + + + + + + +
    126. +
      + +
      +
    127. + + + + + + def self.from_omniauth(auth) +
    128. +
      + +
      +
    129. + + + + + + where(provider: auth.provider, uid: auth.uid).first_or_create do |user| +
    130. +
      + +
      +
    131. + + + + + + user.email = auth.info.email +
    132. +
      + +
      +
    133. + + + + + + user.password = Devise.friendly_token[0, 20] +
    134. +
      + +
      +
    135. + + + + + + user.full_name = auth.info.name # assuming the user model has a name +
    136. +
      + +
      +
    137. + + + + + + end +
    138. +
      + +
      +
    139. + + + + + + end +
    140. +
      + +
      +
    141. + + + + + + +
    142. +
      + +
      +
    143. + + + + + + private +
    144. +
      + +
      +
    145. + + + + + + +
    146. +
      + +
      +
    147. + + + + + + def ensure_an_admin_remains +
    148. +
      + +
      +
    149. + + + + + + if self.is_admin and User.where(is_admin: true).count == 1 +
    150. +
      + +
      +
    151. + + + + + + errors.add :base, "Cannot delete last admin" +
    152. +
      + +
      +
    153. + + + + + + false +
    154. +
      + +
      +
    155. + + + + + + else +
    156. +
      + +
      +
    157. + + + + + + true +
    158. +
      + +
      +
    159. + + + + + + end +
    160. +
      + +
      +
    161. + + + + + + end +
    162. +
      + +
      +
    163. + + + + + + +
    164. +
      + +
      +
    165. + + + + + + def password_complexity +
    166. +
      + +
      +
    167. + + + + + + # Regexp extracted from https://stackoverflow.com/questions/19605150/regex-for-password-must-contain-at-least-eight-characters-at-least-one-number-a +
    168. +
      + +
      +
    169. + + + + + + return if password.blank? || password =~ /(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-])/ +
    170. +
      + +
      +
    171. + + + + + + +
    172. +
      + +
      +
    173. + + + + + + errors.add :password, 'Complexity requirement not met. Please use: 1 uppercase, 1 lowercase, 1 digit and 1 special character' +
    174. +
      + +
      +
    175. + + + + + + end +
    176. +
      + +
      +
    177. + + + + + + +
    178. +
      + +
      +
    179. + + + + + + end +
    180. +
      + +
      +
    181. + + + + + + +
    182. +
      + +
    +
    +
    + + +
    +
    +

    app/models/user_course_membership.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 46 relevant lines. + 0 lines covered and + 46 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + class UserCourseMembership < ActiveRecord::Base +
    36. +
      + +
      +
    37. + + + + + + # UserCourseMembership#role Constants +
    38. +
      + +
      +
    39. + + + + + + # Changes to these constants affect the java library files +
    40. +
      + +
      +
    41. + + + + + + ROLE_TEACHING_STAFF = 0 +
    42. +
      + +
      +
    43. + + + + + + ROLE_TEACHING_ASSISTANT = 1 +
    44. +
      + +
      +
    45. + + + + + + ROLE_STUDENT = 2 +
    46. +
      + +
      +
    47. + + + + + + ROLE_GUEST = 3 +
    48. +
      + +
      +
    49. + + + + + + ROLE_STRINGS = [ +
    50. +
      + +
      +
    51. + + + + + + "Teaching Staff", +
    52. +
      + +
      +
    53. + + + + + + "Teaching Assistant", +
    54. +
      + +
      +
    55. + + + + + + "Student", +
    56. +
      + +
      +
    57. + + + + + + "Guest" +
    58. +
      + +
      +
    59. + + + + + + ] +
    60. +
      + +
      +
    61. + + + + + + +
    62. +
      + +
      +
    63. + + + + + + belongs_to :user +
    64. +
      + +
      +
    65. + + + + + + belongs_to :course +
    66. +
      + +
      +
    67. + + + + + + +
    68. +
      + +
      +
    69. + + + + + + validate :is_existing_user +
    70. +
      + +
      +
    71. + + + + + + validates_uniqueness_of :user_id, scope: [ :course_id ] +
    72. +
      + +
      +
    73. + + + + + + +
    74. +
      + +
      +
    75. + + + + + + +
    76. +
      + +
      +
    77. + + + + + + def role_string +
    78. +
      + +
      +
    79. + + + + + + ROLE_STRINGS[self.role] +
    80. +
      + +
      +
    81. + + + + + + end +
    82. +
      + +
      +
    83. + + + + + + +
    84. +
      + +
      +
    85. + + + + + + def self.options_for_non_student_roles +
    86. +
      + +
      +
    87. + + + + + + ROLE_STRINGS.each_with_index.reject { |role_string, role_id| +
    88. +
      + +
      +
    89. + + + + + + role_id == ROLE_STUDENT +
    90. +
      + +
      +
    91. + + + + + + } +
    92. +
      + +
      +
    93. + + + + + + end +
    94. +
      + +
      +
    95. + + + + + + +
    96. +
      + +
      +
    97. + + + + + + def user_email +
    98. +
      + +
      +
    99. + + + + + + self.user.nil? ? "" : self.user.email +
    100. +
      + +
      +
    101. + + + + + + end +
    102. +
      + +
      +
    103. + + + + + + +
    104. +
      + +
      +
    105. + + + + + + def is_existing_user +
    106. +
      + +
      +
    107. + + + + + + if self.user.nil? +
    108. +
      + +
      +
    109. + + + + + + errors.add :user_email, "This user email is not in SSID. Please invite him or her to signup." +
    110. +
      + +
      +
    111. + + + + + + return false +
    112. +
      + +
      +
    113. + + + + + + end +
    114. +
      + +
      +
    115. + + + + + + end +
    116. +
      + +
      +
    117. + + + + + + +
    118. +
      + +
      +
    119. + + + + + + end +
    120. +
      + +
    +
    +
    + + +
    +
    +

    app/services/courses_service.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 22 relevant lines. + 0 lines covered and + 22 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + class CoursesService +
    2. +
      + +
      +
    3. + + + + + + def self.create_guest_membership(course, user) +
    4. +
      + +
      +
    5. + + + + + + membership = UserCourseMembership.new { |m| +
    6. +
      + +
      +
    7. + + + + + + m.role = 3 +
    8. +
      + +
      +
    9. + + + + + + m.user = user +
    10. +
      + +
      +
    11. + + + + + + m.course = course +
    12. +
      + +
      +
    13. + + + + + + } +
    14. +
      + +
      +
    15. + + + + + + raise ActiveRecord::RecordNotSaved unless membership.save! +
    16. +
      + +
      +
    17. + + + + + + end +
    18. +
      + +
      +
    19. + + + + + + +
    20. +
      + +
      +
    21. + + + + + + def self.create_guest_detail_entry(assignment, user) +
    22. +
      + +
      +
    23. + + + + + + course = assignment.course +
    24. +
      + +
      +
    25. + + + + + + if course +
    26. +
      + +
      +
    27. + + + + + + # create entry under guest user +
    28. +
      + +
      +
    29. + + + + + + guest_detail = GuestUsersDetail.new { |detail| +
    30. +
      + +
      +
    31. + + + + + + detail.user_id = user.id +
    32. +
      + +
      +
    33. + + + + + + detail.course_id = course.id +
    34. +
      + +
      +
    35. + + + + + + detail.hash_string = user.full_name +
    36. +
      + +
      +
    37. + + + + + + detail.assignment_id = assignment.id +
    38. +
      + +
      +
    39. + + + + + + } +
    40. +
      + +
      +
    41. + + + + + + raise ActiveRecord::RecordNotSaved unless guest_detail.save! +
    42. +
      + +
      +
    43. + + + + + + end +
    44. +
      + +
      +
    45. + + + + + + end +
    46. +
      + +
      +
    47. + + + + + + end +
    48. +
      + +
    +
    +
    + + +
    +
    +

    lib/string_calculator.rb

    +

    + + 83.33% + + + lines covered +

    + + + +
    + 6 relevant lines. + 5 lines covered and + 1 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + # lib/string_calculator.rb +
    2. +
      + +
      +
    3. + 1 + + + + + class StringCalculator +
    4. +
      + +
      +
    5. + 2 + + + + + def self.add(input) +
    6. +
      + +
      +
    7. + 2 + + + + + if input.empty? +
    8. +
      + +
      +
    9. + + + + + + 0 +
    10. +
      + +
      +
    11. + + + + + + else +
    12. +
      + +
      +
    13. + 6 + + + + + numbers = input.split(",").map { |num| num.to_i } +
    14. +
      + +
      +
    15. + 6 + + + + + numbers.inject(0) { |sum, number| sum + number } +
    16. +
      + +
      +
    17. + + + + + + end +
    18. +
      + +
      +
    19. + + + + + + end +
    20. +
      + +
      +
    21. + + + + + + end +
    22. +
      + +
    +
    +
    + + +
    +
    +

    lib/submissions_handler.rb

    +

    + + 0.0% + + + lines covered +

    + + + +
    + 214 relevant lines. + 0 lines covered and + 214 lines missed. +
    + + + +
    + +
    +    
      + +
      +
    1. + + + + + + =begin +
    2. +
      + +
      +
    3. + + + + + + This file is part of SSID. +
    4. +
      + +
      +
    5. + + + + + + +
    6. +
      + +
      +
    7. + + + + + + SSID is free software: you can redistribute it and/or modify +
    8. +
      + +
      +
    9. + + + + + + it under the terms of the GNU Lesser General Public License as published by +
    10. +
      + +
      +
    11. + + + + + + the Free Software Foundation, either version 3 of the License, or +
    12. +
      + +
      +
    13. + + + + + + (at your option) any later version. +
    14. +
      + +
      +
    15. + + + + + + +
    16. +
      + +
      +
    17. + + + + + + SSID is distributed in the hope that it will be useful, +
    18. +
      + +
      +
    19. + + + + + + but WITHOUT ANY WARRANTY; without even the implied warranty of +
    20. +
      + +
      +
    21. + + + + + + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +
    22. +
      + +
      +
    23. + + + + + + GNU Lesser General Public License for more details. +
    24. +
      + +
      +
    25. + + + + + + +
    26. +
      + +
      +
    27. + + + + + + You should have received a copy of the GNU Lesser General Public License +
    28. +
      + +
      +
    29. + + + + + + along with SSID. If not, see <http://www.gnu.org/licenses/>. +
    30. +
      + +
      +
    31. + + + + + + =end +
    32. +
      + +
      +
    33. + + + + + + +
    34. +
      + +
      +
    35. + + + + + + require 'zip' +
    36. +
      + +
      +
    37. + + + + + + require 'open3' +
    38. +
      + +
      +
    39. + + + + + + +
    40. +
      + +
      +
    41. + + + + + + class ReorgBot +
    42. +
      + +
      +
    43. + + + + + + +
    44. +
      + +
      +
    45. + + + + + + attr_accessor :path +
    46. +
      + +
      +
    47. + + + + + + attr_accessor :dir +
    48. +
      + +
      +
    49. + + + + + + +
    50. +
      + +
      +
    51. + + + + + + def initialize(path) +
    52. +
      + +
      +
    53. + + + + + + raise ArgumentError, "path '#{path}' does not exist!" unless File.exist?(path) +
    54. +
      + +
      +
    55. + + + + + + raise ArgumentError, "path '#{path} is not a directory you dolt!" unless File.directory?(path) +
    56. +
      + +
      +
    57. + + + + + + @path = path +
    58. +
      + +
      +
    59. + + + + + + @dir = Dir.new(path) +
    60. +
      + +
      +
    61. + + + + + + end +
    62. +
      + +
      +
    63. + + + + + + +
    64. +
      + +
      +
    65. + + + + + + def dirs +
    66. +
      + +
      +
    67. + + + + + + @dir.select { |f| File.directory?(File.join(@path, f)) } +
    68. +
      + +
      +
    69. + + + + + + end +
    70. +
      + +
      +
    71. + + + + + + +
    72. +
      + +
      +
    73. + + + + + + def empty_dirs +
    74. +
      + +
      +
    75. + + + + + + dirs.select { |d| Dir[File.join(@path, d, '*')].empty? } +
    76. +
      + +
      +
    77. + + + + + + end +
    78. +
      + +
      +
    79. + + + + + + +
    80. +
      + +
      +
    81. + + + + + + def non_empty_dirs +
    82. +
      + +
      +
    83. + + + + + + dirs - empty_dirs +
    84. +
      + +
      +
    85. + + + + + + end +
    86. +
      + +
      +
    87. + + + + + + +
    88. +
      + +
      +
    89. + + + + + + def remove_empty_dirs +
    90. +
      + +
      +
    91. + + + + + + log = []; +
    92. +
      + +
      +
    93. + + + + + + empty_dirs.each do |d| +
    94. +
      + +
      +
    95. + + + + + + FileUtils.rm_rf(File.join(@path, d)) +
    96. +
      + +
      +
    97. + + + + + + log << %Q{[#{Time.now.in_time_zone}] Deleting directory: #{File.join(@path, d)}} +
    98. +
      + +
      +
    99. + + + + + + end +
    100. +
      + +
      +
    101. + + + + + + log +
    102. +
      + +
      +
    103. + + + + + + end +
    104. +
      + +
      +
    105. + + + + + + end +
    106. +
      + +
      +
    107. + + + + + + +
    108. +
      + +
      +
    109. + + + + + + module SubmissionsHandler +
    110. +
      + +
      +
    111. + + + + + + +
    112. +
      + +
      +
    113. + + + + + + # Params for data truncation +
    114. +
      + +
      +
    115. + + + + + + MAX_DATA_CHAR_SIZE = 64000 +
    116. +
      + +
      +
    117. + + + + + + DATA_TRUNCATE_MSG = "... (Data got truncated)" +
    118. +
      + +
      +
    119. + + + + + + +
    120. +
      + +
      +
    121. + + + + + + def self.process_upload(file, isMapEnabled, mapfile, assignment) +
    122. +
      + +
      +
    123. + + + + + + upload_dir = File.join(".", "upload", assignment.id.to_s) +
    124. +
      + +
      +
    125. + + + + + + +
    126. +
      + +
      +
    127. + + + + + + # Clear upload dir if exists +
    128. +
      + +
      +
    129. + + + + + + FileUtils.remove_dir upload_dir if File.exist? upload_dir +
    130. +
      + +
      +
    131. + + + + + + +
    132. +
      + +
      +
    133. + + + + + + # Create upload dir +
    134. +
      + +
      +
    135. + + + + + + FileUtils.mkdir_p(upload_dir) +
    136. +
      + +
      +
    137. + + + + + + +
    138. +
      + +
      +
    139. + + + + + + # Keep log +
    140. +
      + +
      +
    141. + + + + + + upload_log = [] +
    142. +
      + +
      +
    143. + + + + + + upload_log << assignment.upload_log.truncate(MAX_DATA_CHAR_SIZE, separator: ' ', omission: DATA_TRUNCATE_MSG) if assignment.upload_log +
    144. +
      + +
      +
    145. + + + + + + upload_log << %Q{[#{Time.now.in_time_zone}] Received file: #{file.original_filename}} +
    146. +
      + +
      +
    147. + + + + + + +
    148. +
      + +
      +
    149. + + + + + + # Rename upload to original file name +
    150. +
      + +
      +
    151. + + + + + + upload_file = File.join(upload_dir, file.original_filename) +
    152. +
      + +
      +
    153. + + + + + + +
    154. +
      + +
      +
    155. + + + + + + # Move upload into dir +
    156. +
      + +
      +
    157. + + + + + + FileUtils.copy_entry(file.path, upload_file) +
    158. +
      + +
      +
    159. + + + + + + +
    160. +
      + +
      +
    161. + + + + + + # Add filters for file types +
    162. +
      + +
      +
    163. + + + + + + accepted_formats = [".ipynb", ".py",".java", ".cpp", ".c", ".h", ".scala", ".m", ".ml", ".mli", ".r"] +
    164. +
      + +
      +
    165. + + + + + + +
    166. +
      + +
      +
    167. + + + + + + # Regexes for special characters (emojis etc.) be removed +
    168. +
      + +
      +
    169. + + + + + + regexes_to_remove = [/[\u{1f600}-\u{1f64f}]/,/[\u{2702}-\u{27b0}]/,/[\u{1f680}-\u{1f6ff}]/,/[\u{24C2}-\u{1F251}]/,/[\u{1f300}-\u{1f5ff}]/] +
    170. +
      + +
      +
    171. + + + + + + +
    172. +
      + +
      +
    173. + + + + + + # check zip file +
    174. +
      + +
      +
    175. + + + + + + has_entry_same_name_with_upload_file = false +
    176. +
      + +
      +
    177. + + + + + + upload_file_without_ext = File.basename(upload_file, ".zip") + File::SEPARATOR +
    178. +
      + +
      +
    179. + + + + + + +
    180. +
      + +
      +
    181. + + + + + + # Extract submissions into dir +
    182. +
      + +
      +
    183. + + + + + + Zip::File.open(upload_file) { |zip_file| +
    184. +
      + +
      +
    185. + + + + + + zip_file.each { |f| +
    186. +
      + +
      +
    187. + + + + + + +
    188. +
      + +
      +
    189. + + + + + + file_entry_names = zip_file.entries.collect {|file| file.name} +
    190. +
      + +
      +
    191. + + + + + + file_entry_names.each { |file_name| +
    192. +
      + +
      +
    193. + + + + + + if (file_name.eql?(upload_file_without_ext)) +
    194. +
      + +
      +
    195. + + + + + + has_entry_same_name_with_upload_file = true +
    196. +
      + +
      +
    197. + + + + + + end +
    198. +
      + +
      +
    199. + + + + + + } +
    200. +
      + +
      +
    201. + + + + + + +
    202. +
      + +
      +
    203. + + + + + + if has_entry_same_name_with_upload_file +
    204. +
      + +
      +
    205. + + + + + + return false +
    206. +
      + +
      +
    207. + + + + + + end +
    208. +
      + +
      +
    209. + + + + + + +
    210. +
      + +
      +
    211. + + + + + + # isdirectory or filter by accepted file extension. Skip __MACOSX/* and .DS_Store generated by Mac. +
    212. +
      + +
      +
    213. + + + + + + if (not f.name.match? /__MACOSX\/|.DS_Store/) and (File.directory?(f.name) or accepted_formats.include? File.extname(f.name)) +
    214. +
      + +
      +
    215. + + + + + + upload_log << %Q{[#{Time.now.in_time_zone}] Extracting #{f.name}} +
    216. +
      + +
      +
    217. + + + + + + # Obtain File Path +
    218. +
      + +
      +
    219. + + + + + + f_path = File.join(upload_dir, f.name) +
    220. +
      + +
      +
    221. + + + + + + # Create Directory +
    222. +
      + +
      +
    223. + + + + + + FileUtils.mkdir_p(File.dirname(f_path)) +
    224. +
      + +
      +
    225. + + + + + + # Extract files into the file path +
    226. +
      + +
      +
    227. + + + + + + zip_file.extract(f, f_path) unless File.exist?(f_path) +
    228. +
      + +
      +
    229. + + + + + + +
    230. +
      + +
      +
    231. + + + + + + # Remove characters in regexes_to_remove from extracted file +
    232. +
      + +
      +
    233. + + + + + + extracted_file_content = File.open(f_path, "r:UTF-8", &:read) +
    234. +
      + +
      +
    235. + + + + + + +
    236. +
      + +
      +
    237. + + + + + + File.open(f_path, "w:UTF-8") do |f| +
    238. +
      + +
      +
    239. + + + + + + +
    240. +
      + +
      +
    241. + + + + + + regexes_to_remove.each do |regex| +
    242. +
      + +
      +
    243. + + + + + + extracted_file_content = extracted_file_content.gsub(regex, '') +
    244. +
      + +
      +
    245. + + + + + + end +
    246. +
      + +
      +
    247. + + + + + + +
    248. +
      + +
      +
    249. + + + + + + f.write(extracted_file_content) +
    250. +
      + +
      +
    251. + + + + + + end +
    252. +
      + +
      +
    253. + + + + + + +
    254. +
      + +
      +
    255. + + + + + + # Reject files that passed the extension test but might be a binary file in disguise +
    256. +
      + +
      +
    257. + + + + + + # if f.file? filepath +
    258. +
      + +
      +
    259. + + + + + + # upload_log << %Q{[#{Time.now.in_time_zone}] Detected binary file, deleting #{f.name}} +
    260. +
      + +
      +
    261. + + + + + + # FileUtils.rm filepath +
    262. +
      + +
      +
    263. + + + + + + # end +
    264. +
      + +
      +
    265. + + + + + + else +
    266. +
      + +
      +
    267. + + + + + + upload_log << %Q{[#{Time.now.in_time_zone}] Invalid file type, Ignoring #{f.name} with extension #{File.extname(f.name)}} +
    268. +
      + +
      +
    269. + + + + + + end +
    270. +
      + +
      +
    271. + + + + + + } +
    272. +
      + +
      +
    273. + + + + + + } +
    274. +
      + +
      +
    275. + + + + + + +
    276. +
      + +
      +
    277. + + + + + + upload_log << %Q{[#{Time.now.in_time_zone}] Checking for empty directories} +
    278. +
      + +
      +
    279. + + + + + + bot = ReorgBot.new(upload_dir) +
    280. +
      + +
      +
    281. + + + + + + upload_log << bot.remove_empty_dirs +
    282. +
      + +
      +
    283. + + + + + + +
    284. +
      + +
      +
    285. + + + + + + # Move map file (if uploaded by user) into dir +
    286. +
      + +
      +
    287. + + + + + + if (isMapEnabled) +
    288. +
      + +
      +
    289. + + + + + + upload_map_file = File.join(upload_dir, "mapfile.csv") +
    290. +
      + +
      +
    291. + + + + + + FileUtils.copy_entry(mapfile.path, upload_map_file) +
    292. +
      + +
      +
    293. + + + + + + end +
    294. +
      + +
      +
    295. + + + + + + +
    296. +
      + +
      +
    297. + + + + + + +
    298. +
      + +
      +
    299. + + + + + + # Save log +
    300. +
      + +
      +
    301. + + + + + + upload_log << %Q{[#{Time.now.in_time_zone}] Unzip complete} +
    302. +
      + +
      +
    303. + + + + + + assignment.upload_log = upload_log.join("\n") +
    304. +
      + +
      +
    305. + + + + + + assignment.save +
    306. +
      + +
      +
    307. + + + + + + +
    308. +
      + +
      +
    309. + + + + + + # Remove zip file +
    310. +
      + +
      +
    311. + + + + + + FileUtils.rm upload_file, force: true +
    312. +
      + +
      +
    313. + + + + + + +
    314. +
      + +
      +
    315. + + + + + + # Return path to dir +
    316. +
      + +
      +
    317. + + + + + + upload_dir +
    318. +
      + +
      +
    319. + + + + + + end +
    320. +
      + +
      +
    321. + + + + + + +
    322. +
      + +
      +
    323. + + + + + + def self.process_submissions(path, assignment, isMapEnabled, used_fingerprints) +
    324. +
      + +
      +
    325. + + + + + + # Create directory for code comparison, delete first if necessary +
    326. +
      + +
      +
    327. + + + + + + compare_dir = File.join(path, "_compare") +
    328. +
      + +
      +
    329. + + + + + + FileUtils.rm(compare_dir, force: true) if File.exist? compare_dir +
    330. +
      + +
      +
    331. + + + + + + FileUtils.mkdir_p(File.join(path, "_compare")) +
    332. +
      + +
      +
    333. + + + + + + +
    334. +
      + +
      +
    335. + + + + + + # For each student submission, combine the code files into one +
    336. +
      + +
      +
    337. + + + + + + Dir.glob(File.join(path, "*")).each { |subpath| +
    338. +
      + +
      +
    339. + + + + + + next if subpath == compare_dir || (File.file?(subpath) && subpath.split('.').last.to_s.downcase == 'csv') +
    340. +
      + +
      +
    341. + + + + + + +
    342. +
      + +
      +
    343. + + + + + + # Combine code files and write into compare dir as new file with same name, remove ext if any +
    344. +
      + +
      +
    345. + + + + + + File.open(File.join(compare_dir, File.basename(subpath, File.extname(subpath))), 'w') { |f| +
    346. +
      + +
      +
    347. + + + + + + f.puts string_from_combined_files(subpath) +
    348. +
      + +
      +
    349. + + + + + + } +
    350. +
      + +
      +
    351. + + + + + + } +
    352. +
      + +
      +
    353. + + + + + + +
    354. +
      + +
      +
    355. + + + + + + if ApplicationHelper.is_application_healthy() +
    356. +
      + +
      +
    357. + + + + + + puts "Process assignment: #{assignment.id}" +
    358. +
      + +
      +
    359. + + + + + + fork_Java_process(compare_dir, assignment, isMapEnabled, used_fingerprints) +
    360. +
      + +
      +
    361. + + + + + + else +
    362. +
      + +
      +
    363. + + + + + + puts "Put in queue, assignment: #{assignment.id}" +
    364. +
      + +
      +
    365. + + + + + + SubmissionSimilarityProcess.create do |p| +
    366. +
      + +
      +
    367. + + + + + + p.assignment_id = assignment.id +
    368. +
      + +
      +
    369. + + + + + + p.status = SubmissionSimilarityProcess::STATUS_WAITING +
    370. +
      + +
      +
    371. + + + + + + end +
    372. +
      + +
      +
    373. + + + + + + end +
    374. +
      + +
      +
    375. + + + + + + end +
    376. +
      + +
      +
    377. + + + + + + +
    378. +
      + +
      +
    379. + + + + + + def self.fork_Java_process(compare_dir, assignment, isMapEnabled, used_fingerprints) +
    380. +
      + +
      +
    381. + + + + + + # Read database configuration +
    382. +
      + +
      +
    383. + + + + + + config = Rails.configuration.database_configuration +
    384. +
      + +
      +
    385. + + + + + + host = config[Rails.env]["host"] +
    386. +
      + +
      +
    387. + + + + + + database = config[Rails.env]["database"] +
    388. +
      + +
      +
    389. + + + + + + username = config[Rails.env]["username"] +
    390. +
      + +
      +
    391. + + + + + + password = config[Rails.env]["password"] +
    392. +
      + +
      +
    393. + + + + + + +
    394. +
      + +
      +
    395. + + + + + + # Run the java program and get its pid +
    396. +
      + +
      +
    397. + + + + + + command = %Q{java -Xmx2048M -Dlog4j2.configurationFile="#{Rails.application.config.plagiarism_detection_log_configuration_path}" -jar "#{Rails.application.config.plagiarism_detection_path}" } + +
    398. +
      + +
      +
    399. + + + + + + %Q{#{assignment.id} #{compare_dir} #{assignment.language.downcase} } + +
    400. +
      + +
      +
    401. + + + + + + %Q{#{assignment.min_match_length} #{assignment.ngram_size} } + +
    402. +
      + +
      +
    403. + + + + + + %Q{#{host} #{database} #{username} #{password} #{isMapEnabled} #{used_fingerprints}} +
    404. +
      + +
      +
    405. + + + + + + # Fork to run java program in background +
    406. +
      + +
      +
    407. + + + + + + ruby_pid = Process.fork do +
    408. +
      + +
      +
    409. + + + + + + java_log = "" +
    410. +
      + +
      +
    411. + + + + + + java_status = nil +
    412. +
      + +
      +
    413. + + + + + + Open3.popen2e({ "LD_LIBRARY_PATH" => Rails.application.config.ld_library_path }, command) { |i,o,t| +
    414. +
      + +
      +
    415. + + + + + + java_log << o.gets until o.eof? +
    416. +
      + +
      +
    417. + + + + + + java_status = t.value +
    418. +
      + +
      +
    419. + + + + + + } +
    420. +
      + +
      +
    421. + + + + + + +
    422. +
      + +
      +
    423. + + + + + + # Update log +
    424. +
      + +
      +
    425. + + + + + + upload_log = [] +
    426. +
      + +
      +
    427. + + + + + + upload_log << assignment.upload_log if assignment.upload_log +
    428. +
      + +
      +
    429. + + + + + + upload_log << java_log.truncate(MAX_DATA_CHAR_SIZE, separator: ' ', omission: DATA_TRUNCATE_MSG) +
    430. +
      + +
      +
    431. + + + + + + assignment.upload_log = upload_log.join("\n") +
    432. +
      + +
      +
    433. + + + + + + +
    434. +
      + +
      +
    435. + + + + + + # Update status +
    436. +
      + +
      +
    437. + + + + + + process = assignment.submission_similarity_process +
    438. +
      + +
      +
    439. + + + + + + if java_status.exitstatus == 0 +
    440. +
      + +
      +
    441. + + + + + + process.status = SubmissionSimilarityProcess::STATUS_COMPLETED +
    442. +
      + +
      +
    443. + + + + + + else +
    444. +
      + +
      +
    445. + + + + + + process.status = SubmissionSimilarityProcess::STATUS_ERRONEOUS +
    446. +
      + +
      +
    447. + + + + + + puts "Print out log in case of erroneous processing" +
    448. +
      + +
      +
    449. + + + + + + puts java_log +
    450. +
      + +
      +
    451. + + + + + + end +
    452. +
      + +
      +
    453. + + + + + + +
    454. +
      + +
      +
    455. + + + + + + # Save +
    456. +
      + +
      +
    457. + + + + + + assignment.transaction do +
    458. +
      + +
      +
    459. + + + + + + assignment.save +
    460. +
      + +
      +
    461. + + + + + + process.save +
    462. +
      + +
      +
    463. + + + + + + end +
    464. +
      + +
      +
    465. + + + + + + end +
    466. +
      + +
      +
    467. + + + + + + +
    468. +
      + +
      +
    469. + + + + + + # Create process with pid +
    470. +
      + +
      +
    471. + + + + + + process = assignment.submission_similarity_process +
    472. +
      + +
      +
    473. + + + + + + if process.nil? +
    474. +
      + +
      +
    475. + + + + + + SubmissionSimilarityProcess.create do |p| +
    476. +
      + +
      +
    477. + + + + + + p.assignment_id = assignment.id +
    478. +
      + +
      +
    479. + + + + + + p.pid = ruby_pid +
    480. +
      + +
      +
    481. + + + + + + p.status = SubmissionSimilarityProcess::STATUS_RUNNING +
    482. +
      + +
      +
    483. + + + + + + end +
    484. +
      + +
      +
    485. + + + + + + else +
    486. +
      + +
      +
    487. + + + + + + process.pid = ruby_pid +
    488. +
      + +
      +
    489. + + + + + + process.status = SubmissionSimilarityProcess::STATUS_RUNNING +
    490. +
      + +
      +
    491. + + + + + + process.save +
    492. +
      + +
      +
    493. + + + + + + end +
    494. +
      + +
      +
    495. + + + + + + +
    496. +
      + +
      +
    497. + + + + + + Process.detach(ruby_pid) # Parent will not wait +
    498. +
      + +
      +
    499. + + + + + + end +
    500. +
      + +
      +
    501. + + + + + + +
    502. +
      + +
      +
    503. + + + + + + def self.process_cluster_group(cluster_group) +
    504. +
      + +
      +
    505. + + + + + + # Get assignment +
    506. +
      + +
      +
    507. + + + + + + assignment = cluster_group.assignment +
    508. +
      + +
      +
    509. + + + + + + +
    510. +
      + +
      +
    511. + + + + + + # Read database configuration +
    512. +
      + +
      +
    513. + + + + + + config = Rails.configuration.database_configuration +
    514. +
      + +
      +
    515. + + + + + + host = config[Rails.env]["host"] +
    516. +
      + +
      +
    517. + + + + + + database = config[Rails.env]["database"] +
    518. +
      + +
      +
    519. + + + + + + username = config[Rails.env]["username"] +
    520. +
      + +
      +
    521. + + + + + + password = config[Rails.env]["password"] +
    522. +
      + +
      +
    523. + + + + + + +
    524. +
      + +
      +
    525. + + + + + + # Run the java program and get its pid +
    526. +
      + +
      +
    527. + + + + + + command = %Q{java -Xmx1024M -jar "#{Rails.application.config.submissions_clustering_path}" } + +
    528. +
      + +
      +
    529. + + + + + + %Q{#{assignment.id} #{cluster_group.id} #{cluster_group.cut_off_criterion} } + +
    530. +
      + +
      +
    531. + + + + + + %Q{#{host} #{database} #{username} #{password} 2>&1} +
    532. +
      + +
      +
    533. + + + + + + +
    534. +
      + +
      +
    535. + + + + + + java_log = "" +
    536. +
      + +
      +
    537. + + + + + + IO.popen(command) { |pipe| +
    538. +
      + +
      +
    539. + + + + + + java_log << pipe.gets until pipe.eof? +
    540. +
      + +
      +
    541. + + + + + + } +
    542. +
      + +
      +
    543. + + + + + + java_status = $? +
    544. +
      + +
      +
    545. + + + + + + +
    546. +
      + +
      +
    547. + + + + + + raise "Submissions clustering error: #{java_log}" unless java_status.exitstatus == 0 +
    548. +
      + +
      +
    549. + + + + + + end +
    550. +
      + +
      +
    551. + + + + + + +
    552. +
      + +
      +
    553. + + + + + + private +
    554. +
      + +
      +
    555. + + + + + + +
    556. +
      + +
      +
    557. + + + + + + def self.string_from_combined_files(path) +
    558. +
      + +
      +
    559. + + + + + + strings = [] +
    560. +
      + +
      +
    561. + + + + + + if File.directory? path +
    562. +
      + +
      +
    563. + + + + + + Dir.glob(File.join(path, "*")).sort.each { |subpath| +
    564. +
      + +
      +
    565. + + + + + + strings << string_from_combined_files(subpath) +
    566. +
      + +
      +
    567. + + + + + + } +
    568. +
      + +
      +
    569. + + + + + + else +
    570. +
      + +
      +
    571. + + + + + + # byebug +
    572. +
      + +
      +
    573. + + + + + + if (File.extname(path).include? ".ipynb") then +
    574. +
      + +
      +
    575. + + + + + + convert_to_python(path, strings) +
    576. +
      + +
      +
    577. + + + + + + else +
    578. +
      + +
      +
    579. + + + + + + strings << File.open(path).readlines.join +
    580. +
      + +
      +
    581. + + + + + + end +
    582. +
      + +
      +
    583. + + + + + + end +
    584. +
      + +
      +
    585. + + + + + + +
    586. +
      + +
      +
    587. + + + + + + strings.join("\n") +
    588. +
      + +
      +
    589. + + + + + + end +
    590. +
      + +
      +
    591. + + + + + + +
    592. +
      + +
      +
    593. + + + + + + def self.convert_to_python(path, strings) +
    594. +
      + +
      +
    595. + + + + + + # byebug +
    596. +
      + +
      +
    597. + + + + + + path_without_special_chars = path.gsub(/[\s\(\)\*\@\$\%\&\*]/, '__') +
    598. +
      + +
      +
    599. + + + + + + File.rename(path, path_without_special_chars) +
    600. +
      + +
      +
    601. + + + + + + command = "jupyter nbconvert --to script #{path_without_special_chars}" +
    602. +
      + +
      +
    603. + + + + + + isConversionSuccessful = system(command) +
    604. +
      + +
      +
    605. + + + + + + if isConversionSuccessful then +
    606. +
      + +
      +
    607. + + + + + + new_path = path_without_special_chars.gsub(/.ipynb$/, '.py') +
    608. +
      + +
      +
    609. + + + + + + strings << File.open(new_path).readlines.join +
    610. +
      + +
      +
    611. + + + + + + else +
    612. +
      + +
      +
    613. + + + + + + puts "Failed to convert file #{path_without_special_chars} to py" +
    614. +
      + +
      +
    615. + + + + + + end +
    616. +
      + +
      +
    617. + + + + + + end +
    618. +
      + +
      +
    619. + + + + + + end +
    620. +
      + +
    +
    +
    + + +
    +
    + + diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 00000000..3ed897bd --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,2390 @@ +SF:./app/controllers/admin/users_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:20,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:29,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:51,0 +DA:52,0 +DA:55,0 +DA:56,0 +DA:59,0 +DA:60,0 +DA:63,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:89,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +end_of_record +SF:./app/controllers/announcements_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:32,0 +DA:33,0 +DA:35,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:85,0 +DA:87,0 +DA:90,0 +DA:91,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:126,0 +DA:127,0 +DA:128,0 +end_of_record +SF:./app/controllers/application_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:59,0 +DA:60,0 +DA:63,0 +DA:64,0 +DA:67,0 +DA:70,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:79,0 +DA:80,0 +DA:83,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:91,0 +DA:92,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:102,0 +DA:104,0 +DA:106,0 +DA:109,0 +DA:110,0 +DA:112,0 +DA:113,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +end_of_record +SF:./app/controllers/assignments_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:20,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:45,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:97,0 +DA:98,0 +DA:100,0 +DA:103,0 +DA:105,0 +DA:106,0 +DA:109,0 +DA:111,0 +DA:113,0 +DA:115,0 +DA:116,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:142,0 +DA:143,0 +DA:145,0 +DA:146,0 +DA:149,0 +DA:150,0 +DA:151,0 +DA:152,0 +DA:153,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:159,0 +DA:160,0 +DA:162,0 +DA:163,0 +DA:164,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:169,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:176,0 +DA:177,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:187,0 +DA:188,0 +DA:190,0 +DA:191,0 +DA:194,0 +DA:195,0 +DA:197,0 +DA:199,0 +DA:200,0 +DA:201,0 +DA:202,0 +DA:203,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:207,0 +DA:208,0 +DA:210,0 +DA:217,0 +DA:219,0 +DA:220,0 +DA:221,0 +DA:222,0 +DA:223,0 +DA:225,0 +DA:226,0 +DA:228,0 +DA:229,0 +DA:230,0 +DA:236,0 +DA:238,0 +DA:239,0 +DA:240,0 +DA:241,0 +DA:242,0 +DA:243,0 +DA:244,0 +DA:245,0 +DA:247,0 +DA:248,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:254,0 +DA:255,0 +DA:256,0 +DA:257,0 +DA:258,0 +DA:259,0 +DA:261,0 +end_of_record +SF:./app/controllers/courses_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:128,0 +DA:129,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:142,0 +end_of_record +SF:./app/controllers/submission_cluster_groups_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:71,0 +DA:72,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:112,0 +DA:113,0 +DA:114,0 +end_of_record +SF:./app/controllers/submission_cluster_memberships_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +end_of_record +SF:./app/controllers/submission_clusters_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +end_of_record +SF:./app/controllers/submission_logs_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:64,0 +end_of_record +SF:./app/controllers/submission_similarities_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:65,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:79,0 +DA:80,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:98,0 +DA:99,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:109,0 +DA:110,0 +DA:112,0 +DA:113,0 +DA:116,0 +DA:117,0 +DA:119,0 +DA:120,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:145,0 +DA:146,0 +DA:147,0 +DA:148,0 +DA:149,0 +DA:150,0 +DA:151,0 +DA:152,0 +DA:153,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:169,0 +DA:170,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:176,0 +DA:177,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:189,0 +DA:190,0 +DA:191,0 +DA:192,0 +DA:193,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:199,0 +DA:200,0 +DA:201,0 +DA:202,0 +DA:203,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:207,0 +DA:208,0 +DA:209,0 +DA:211,0 +DA:213,0 +DA:214,0 +DA:215,0 +DA:216,0 +DA:217,0 +DA:218,0 +DA:219,0 +DA:220,0 +DA:221,0 +DA:222,0 +DA:223,0 +DA:224,0 +DA:225,0 +end_of_record +SF:./app/controllers/submission_similarity_mappings_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +end_of_record +SF:./app/controllers/submission_similarity_processes_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +end_of_record +SF:./app/controllers/submissions_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:64,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:78,0 +DA:79,0 +end_of_record +SF:./app/controllers/user_course_memberships_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:97,0 +DA:98,0 +DA:99,0 +end_of_record +SF:./app/controllers/users/confirmations_controller.rb +DA:3,0 +DA:5,0 +DA:6,0 +DA:8,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:31,0 +end_of_record +SF:./app/controllers/users/omniauth_callbacks_controller.rb +DA:3,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +end_of_record +SF:./app/controllers/users/passwords_controller.rb +DA:3,0 +DA:34,0 +end_of_record +SF:./app/controllers/users/registrations_controller.rb +DA:3,0 +DA:4,0 +DA:5,0 +DA:8,0 +DA:9,0 +DA:10,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:72,0 +end_of_record +SF:./app/controllers/users/sessions_controller.rb +DA:3,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:56,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +end_of_record +SF:./app/controllers/visualize_controller.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:32,0 +DA:33,0 +DA:36,0 +DA:37,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:45,0 +DA:46,0 +DA:47,0 +end_of_record +SF:./app/helpers/application_helper.rb +DA:1,0 +DA:3,0 +DA:4,0 +DA:5,0 +DA:8,0 +DA:9,0 +DA:11,0 +DA:12,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:26,0 +end_of_record +SF:./app/helpers/site_helper.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +end_of_record +SF:./app/helpers/site_link_renderer.rb +DA:1,0 +DA:3,0 +DA:4,0 +DA:6,0 +DA:7,0 +DA:8,0 +DA:9,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:30,0 +end_of_record +SF:./app/helpers/site_modal.rb +DA:1,0 +DA:2,0 +DA:3,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:8,0 +end_of_record +SF:./app/helpers/submission_clusters_helper.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +end_of_record +SF:./app/mailers/application_mailer.rb +DA:1,0 +DA:2,0 +DA:3,0 +DA:4,0 +end_of_record +SF:./app/mailers/user_mailer.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:8,0 +end_of_record +SF:./app/models/announcement.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +end_of_record +SF:./app/models/application_record.rb +DA:1,0 +DA:2,0 +DA:3,0 +end_of_record +SF:./app/models/assignment.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:83,0 +DA:84,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +end_of_record +SF:./app/models/course.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:139,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:144,0 +end_of_record +SF:./app/models/guest_users_detail.rb +DA:1,0 +DA:2,0 +DA:3,0 +DA:4,0 +end_of_record +SF:./app/models/submission.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:25,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +end_of_record +SF:./app/models/submission_cluster.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +end_of_record +SF:./app/models/submission_cluster_group.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +end_of_record +SF:./app/models/submission_cluster_membership.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +end_of_record +SF:./app/models/submission_log.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +end_of_record +SF:./app/models/submission_similarity.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +end_of_record +SF:./app/models/submission_similarity_mapping.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +end_of_record +SF:./app/models/submission_similarity_process.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:27,0 +end_of_record +SF:./app/models/submission_similarity_skeleton_mapping.rb +DA:1,0 +DA:2,0 +DA:4,0 +end_of_record +SF:./app/models/user.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:72,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:83,0 +DA:85,0 +DA:87,0 +DA:88,0 +DA:90,0 +end_of_record +SF:./app/models/user_course_membership.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:60,0 +end_of_record +SF:./app/services/courses_service.rb +DA:1,0 +DA:2,0 +DA:3,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:8,0 +DA:9,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +end_of_record +SF:./lib/string_calculator.rb +DA:2,1 +DA:3,1 +DA:4,2 +DA:5,0 +DA:7,6 +DA:8,6 +end_of_record +SF:./lib/submissions_handler.rb +DA:1,0 +DA:2,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:55,0 +DA:58,0 +DA:59,0 +DA:61,0 +DA:62,0 +DA:65,0 +DA:68,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:76,0 +DA:79,0 +DA:82,0 +DA:85,0 +DA:88,0 +DA:89,0 +DA:92,0 +DA:93,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:107,0 +DA:108,0 +DA:110,0 +DA:112,0 +DA:114,0 +DA:117,0 +DA:119,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:125,0 +DA:126,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:144,0 +DA:145,0 +DA:146,0 +DA:147,0 +DA:151,0 +DA:152,0 +DA:153,0 +DA:156,0 +DA:159,0 +DA:160,0 +DA:162,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:169,0 +DA:170,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:176,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:188,0 +DA:190,0 +DA:192,0 +DA:193,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:199,0 +DA:200,0 +DA:201,0 +DA:202,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:207,0 +DA:208,0 +DA:209,0 +DA:210,0 +DA:213,0 +DA:214,0 +DA:215,0 +DA:216,0 +DA:219,0 +DA:220,0 +DA:221,0 +DA:222,0 +DA:223,0 +DA:224,0 +DA:225,0 +DA:226,0 +DA:229,0 +DA:230,0 +DA:231,0 +DA:232,0 +DA:233,0 +DA:236,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:240,0 +DA:241,0 +DA:242,0 +DA:243,0 +DA:244,0 +DA:245,0 +DA:246,0 +DA:247,0 +DA:249,0 +DA:250,0 +DA:252,0 +DA:254,0 +DA:257,0 +DA:258,0 +DA:259,0 +DA:260,0 +DA:261,0 +DA:264,0 +DA:265,0 +DA:266,0 +DA:268,0 +DA:269,0 +DA:270,0 +DA:271,0 +DA:272,0 +DA:274,0 +DA:275,0 +DA:277,0 +DA:279,0 +DA:280,0 +DA:281,0 +DA:282,0 +DA:283,0 +DA:284,0 +DA:285,0 +DA:287,0 +DA:288,0 +DA:289,0 +DA:290,0 +DA:291,0 +DA:292,0 +DA:294,0 +DA:295,0 +DA:297,0 +DA:299,0 +DA:300,0 +DA:301,0 +DA:302,0 +DA:303,0 +DA:304,0 +DA:305,0 +DA:306,0 +DA:307,0 +DA:308,0 +DA:309,0 +DA:310,0 +end_of_record diff --git a/db/migrate/20221226085504_add_devise_to_users.rb b/db/migrate/20221226085504_add_devise_to_users.rb new file mode 100644 index 00000000..f68e5eac --- /dev/null +++ b/db/migrate/20221226085504_add_devise_to_users.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +class AddDeviseToUsers < ActiveRecord::Migration[6.0] + def self.up + rename_column :users, :password_digest, :encrypted_password + + change_table :users do |t| + ## Database authenticatable + # t.string :email, null: false, default: "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + # t.integer :sign_in_count, default: 0, null: false + # t.datetime :current_sign_in_at + # t.datetime :last_sign_in_at + # t.string :current_sign_in_ip + # t.string :last_sign_in_ip + + ## Confirmable + t.string :confirmation_token + t.datetime :confirmed_at + t.datetime :confirmation_sent_at + t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + + # Uncomment below if timestamps were not included in your original model. + # t.timestamps null: false + end + + # add_index :users, :email, unique: true + add_index :users, :reset_password_token, unique: true + add_index :users, :confirmation_token, unique: true + # add_index :users, :unlock_token, unique: true + end + + def self.down + # By default, we don't want to make any assumption about how to roll back a migration when your + # model already existed. Please edit below which fields you would like to remove in this migration. + # raise ActiveRecord::IrreversibleMigration + rename_column :users, :encrypted_password, :password_digest + change_table :users do |t| + ## Database authenticatable + + ## Recoverable + t.remove :reset_password_token, :reset_password_sent_at + + ## Rememberable + t.remove :remember_created_at + + ## Trackable + # t.remove :sign_in_count, :current_sign_in_at, :last_sign_in_at, :current_sign_in_ip, :last_sign_in_ip + + ## Confirmable + t.remove :confirmation_token, :confirmed_at, :confirmation_sent_at + + ## Lockable + # t.remove :failed_attempts, :unlock_token, :locked_at + + # Uncomment below if timestamps were not included in your original model. + # t.timestamps + end + end +end diff --git a/db/migrate/20221226110335_remove_old_authentication_columns.rb b/db/migrate/20221226110335_remove_old_authentication_columns.rb new file mode 100644 index 00000000..174cadfc --- /dev/null +++ b/db/migrate/20221226110335_remove_old_authentication_columns.rb @@ -0,0 +1,7 @@ +class RemoveOldAuthenticationColumns < ActiveRecord::Migration[6.0] + def change + remove_column :users, :activation_digest + remove_column :users, :activated + remove_column :users, :activated_at + end +end diff --git a/db/migrate/20230219165130_add_omniauth_to_user.rb b/db/migrate/20230219165130_add_omniauth_to_user.rb new file mode 100644 index 00000000..a2e5b120 --- /dev/null +++ b/db/migrate/20230219165130_add_omniauth_to_user.rb @@ -0,0 +1,6 @@ +class AddOmniauthToUser < ActiveRecord::Migration[6.0] + def change + add_column :users, :uid, :string + add_column :users, :provider, :string + end +end diff --git a/db/migrate/20230627085255_remove_password_resets.rb b/db/migrate/20230627085255_remove_password_resets.rb new file mode 100644 index 00000000..4487174f --- /dev/null +++ b/db/migrate/20230627085255_remove_password_resets.rb @@ -0,0 +1,7 @@ +require_relative '20220524040659_create_password_resets.rb' + +class RemovePasswordResets < ActiveRecord::Migration[6.0] + def change + revert CreatePasswordResets + end +end diff --git a/db/migrate/20230916144010_add_api_key_to_users.rb b/db/migrate/20230916144010_add_api_key_to_users.rb new file mode 100644 index 00000000..11b15b23 --- /dev/null +++ b/db/migrate/20230916144010_add_api_key_to_users.rb @@ -0,0 +1,9 @@ +class AddApiKeyToUsers < ActiveRecord::Migration[6.0] + def change + create_table :api_keys do |t| + t.belongs_to :user, index: true, foreign_key: true + t.string :value, index: {unique: true} + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 40dbad07..c0533766 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_06_13_072330) do +ActiveRecord::Schema.define(version: 2023_09_16_144010) do create_table "announcements", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| t.string "title" @@ -22,6 +22,15 @@ t.datetime "updated_at", precision: 6, null: false end + create_table "api_keys", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + t.bigint "user_id" + t.string "value" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["user_id"], name: "index_api_keys_on_user_id" + t.index ["value"], name: "index_api_keys_on_value", unique: true + end + create_table "assignments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| t.string "title", null: false t.string "language", null: false @@ -138,6 +147,17 @@ t.index ["assignment_id"], name: "index_submission_similarity_processes_on_assignment_id" end + create_table "submission_similarity_skeleton_mappings", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + t.bigint "submission_similarity_mapping_id" + t.integer "start_line1" + t.integer "end_line1" + t.integer "start_line2" + t.integer "end_line2" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["submission_similarity_mapping_id"], name: "on_submission_similarity_mapping_id" + end + create_table "submissions", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| t.text "lines", size: :long t.integer "assignment_id", null: false @@ -161,11 +181,25 @@ create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| t.string "name" t.string "full_name" - t.string "password_digest", default: "$2a$12$BEjX4o6KQ8HsgdP7JV6NEuVygw6U5kaKQrvxk9U3xtotcS92.0BlG" + t.string "encrypted_password", default: "$2a$12$IqSmxAasfBz34E97zo1D2eVC6gQGlKb3M7i7ikdzq3CYWatKggRua" t.boolean "is_admin", default: false, null: false - t.string "id_string" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.string "email" + t.boolean "is_admin_approved", default: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.string "confirmation_token" + t.datetime "confirmed_at" + t.datetime "confirmation_sent_at" + t.string "unconfirmed_email" + t.string "uid" + t.string "provider" + t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + add_foreign_key "api_keys", "users" end diff --git a/db/seeds.rb b/db/seeds.rb index aa1ce15e..da4253f2 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -16,14 +16,16 @@ =end # Create default admin user -admin = User.new { |user| - user.name = "admin" +admin = User.new do |user| + user.name = "admin123" user.full_name = "SSID Administrator" - user.password_digest = BCrypt::Password.create('$$SSIDPassword$$') - user.email = "ssidadmin@example.com", - user.is_admin = true, - user.is_admin_approved = true, - user.activated = true, - user.activated_at = Time.zone.now -} + user.password = "SSIDPassword123!" + user.password_confirmation = "SSIDPassword123!" + user.email = "ssidadmin123@example.com" + user.is_admin = true + user.is_admin_approved = true + user.confirmed_at = Time.zone.now + user.confirmation_sent_at = Time.zone.now +end +admin.skip_confirmation! # Used to confirm account when seeding a user to bypass user.confirmed? check admin.save diff --git "a/doc/CALT SSID \342\200\223 A Plagiarism Detection System.mp4" "b/doc/CALT SSID \342\200\223 A Plagiarism Detection System.mp4" new file mode 100644 index 00000000..399627eb Binary files /dev/null and "b/doc/CALT SSID \342\200\223 A Plagiarism Detection System.mp4" differ diff --git a/doc/SSID Competitor Analysis.pdf b/doc/SSID Competitor Analysis.pdf new file mode 100644 index 00000000..2f451eea Binary files /dev/null and b/doc/SSID Competitor Analysis.pdf differ diff --git a/doc/SSID Design System.pdf b/doc/SSID Design System.pdf new file mode 100644 index 00000000..a43a0a65 Binary files /dev/null and b/doc/SSID Design System.pdf differ diff --git a/doc/SSID-Landing-Page-ProjectRoadmap.pdf b/doc/SSID-Landing-Page-ProjectRoadmap.pdf new file mode 100644 index 00000000..607a0e55 Binary files /dev/null and b/doc/SSID-Landing-Page-ProjectRoadmap.pdf differ diff --git a/doc/SSID_API_Documentation.md b/doc/SSID_API_Documentation.md new file mode 100644 index 00000000..b744ce4a --- /dev/null +++ b/doc/SSID_API_Documentation.md @@ -0,0 +1,232 @@ +--- +layout: page +title: SSID API +--- + +# SSID API + +## Table of Contents + +- [Introduction](#introduction) +- [Authentication](#authentication) +- [Functionality](#functionality) + - [Create an assignment](#1-create-an-assignment) + - [View pairwise submissions comparison results](#2-view-pairwise-submissions-comparison-results) + - [View details of flagged pairwise submissions comparison results](#3-view-details-of-flagged-pairwise-submissions-comparison-results) + +## Introduction + +SSID API allows you to programmatically use a subset of SSID's functionalities. With SSID API, you can create assignments and submit assignment files programmatically, retrieve plagiarism detection results in machine-readable format, and integrate SSID into your applications. This document describes the usage of SSID API. + +## Authentication + +- **Creating API keys**: Please contact SSID team to get your developer API key. +- **Adding API key to your request**: You must include your API key with every request in the parameter `X-API-KEY` in your request header. + +## Functionality + +### 1. Create an assignment + +**URL**: `/api/v1/courses/{course_id}/assignment/` + +**Method**: `POST` + +**Authentication required**: YES + +**Description**: Creates a new assignment for SSID to process. The web equivalence is going to `/courses/{course_id}/assignments/new` and creating a new assignment. + +The content type of the request should be `multipart/form-data`. Each parameter is a form part, with the parameter name specified in the `name` attribute in `Content-Disposition` header. In the body, any boundary delimiter for different parts to be sent is acceptable. + +**JSON Parameters**: + +- `title (Required) string`: The title of the assignment. +- `language (Required) string`: Programming language of submission files. Currently supported: `"java", "python3", "c", "cpp", "javascript", "r", "ocaml", "matlab", "scala"`. The parameter value must be in lowercase and match exactly one of the options. +- `useFingerprints (Optional) boolean`: If `true`, enable the optimization of preprocessing batch of submissions using winnowing fingerprinting algorithm before pairwise comparisons. If not specified, defaults to `false`. +- `minimumMatchLength (Optional) number`: The least number of contiguous identical statements required to flag a match. If not specified, defaults to 2. +- `sizeOfNGram (Optional) number`: Specifies size of n-gram to be used in SSID. An n-gram is a contiguous subsequence of n tokens of a given sequence. If not specified, defaults to 5. +- `discardAfter (Optional) number`: If specified, discard files after such duration in seconds. **[Note: Currently SSID stores all data persistently and data is manually cleared every semester. This parameter accommodates the future auto-discard functionality. This function is not yet available in both web interface and API, so we need to first build it for SSID, then allow it to be used via API]**. +- `studentSubmissions (Required) zip`: zip file of student submissions in SSID’s standard format. **[Note: The zip file should be in SSID's standard format (potentially includes skeleton codes). For more details, please refer to SSID's User's Guide. Removed file size limit as there might be users with bigger file sizes.]**. +- `mappingFile (Optional) csv`: csv map file that allows you to map between a directory name (in the uploaded zip file) and the student roster that you might be using for your modules. For more details, see SSID's User Guide. +- `references zip`: zip files for past semesters to be used as reference. Maximum of 5 reference zips allowed. **[Note: This function is not yet available in both web interface and API, so we need to first build it for SSID, then allow it to be used via API.]**. + +**Request Example**: +| Header | +| --- | + +``` +{ + "Content-Type": "multipart/form-data; + \"boundary=------------------------a41c8131a72f1dad\"", + "X-Api-Key": "YOUR_API_KEY" +} +``` + +| Body | +| ---- | + +``` +--------------------------a41c8131a72f1dad +Content-Disposition: form-data; name="title" + +assignment1 +--------------------------a41c8131a72f1dad +Content-Disposition: form-data; name="language" + +java +--------------------------a41c8131a72f1dad +Content-Disposition: form-data; name="sizeOfNGram" + +5 +--------------------------a41c8131a72f1dad +Content-Disposition: form-data; name="studentSubmissions"; filename="dir.zip" +Content-Type: application/octet-stream + + +--------------------------a41c8131a72f1dad– +``` + +**Possible responses**: + +| Code | Status | Return body | +| ---- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 200 | Successful | `{ "assignmentID": 1168 }` | +| 400 | Error | `{ "error": "Value of {parameterName} is not valid. {state the constraint/ limit}" }`, `{ "error": "Missing required parameter {parameterName}" }`, or `{ "error": "Parameter {parameterName} is invalid or not yet supported." }` | +| 401 | Unauthorized | `{ "error": "Missing or invalid API key." }` or `{ "error": "Your API key is not authorized to access this resource." }` | +| 503 | Service Unavailable | `{ "error": "SSID is busy or under maintenance. Please try again later." }` | + +--- + +### 2. View pairwise submissions comparison results + +**URL**: `/api/v1/assignments/{assignment_id}/submission_similarities/` + +***With threshold***: `/api/v1/assignments/{assignment_id}/submission_similarities?threshold={threshold value}`. For example, `/api/v1/assignments/{assignment_id}/submission_similarities?threshold=95` returns all submission similarities whose similarity is greater than or equal to 95%. + +***With limit***: `/api/v1/assignments/{assignment_id}/submission_similarities?limit={limit count}`. For example, `/api/v1/assignments/{assignment_id}/submission_similarities?limit=5` returns 5 submission similarities. + +**Method**: `GET` + +**Authentication required**: YES + +**Description**: Returns all submission similarities of an assignment. The web equivalence is going to `/assignments/{assignment_id}/submission_similarities/`. + +**JSON Parameters**: + + +- `threshold (Optional) number`: A number between 0 and 100. If specified, returns only submission similarities whose similarity percentage is between `threshold` and 100 inclusive. Otherwise, returns all submission similarities **[Note: Currently SSID stores and displays all submission similarities on its web interface. The function to display only submission similarities whose similarity percentage is greater than or equal to `threshold` is not yet available in the web interface so we need to build it for SSID]**. +- `limit (Optional) number`: If specified, returns such number of submission similarities with highest maximum similarity percentage. Otherwise, returns all submission similarities. **[Note: Currently SSID stores and displays all submission similarities on its web interface. The function to take top N submissions is not yet available in the web interface so we need to build it for SSID]**. + +**Request Example**: +| Header | +| --- | + +``` +{ + "Content-Type": "application/json", + "X-Api-Key": "YOUR_API_KEY" +} +``` + +| Body | +| ---- | + +``` + +``` + +**Possible responses**: + +| Code | Status | Return body | +| ---- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 200 | Successful | `{ "status": "empty" }` **[Note: This status, currently in SSID's web interface, is for when the user did not include a zip file when creating an assignment. To rectify this, the user must upload the file via the web interface. When submitting through API, the zip assignment file is compulsory.]**, `{ "status": "processing" }`, or `{ "status": "processed", "submissionSimilarities": [] }` | +| 400 | Error | `{ “error”: “Submission similarities requested does not exist." }`, `{ “error”: “Assignment does not exist" }` | +| 401 | Unauthorized | `{ "error": "Missing or invalid API key." }` or `{ "error": "Your API key is not authorized to access this resource." }` | +| 503 | Service Unavailable | `{ "error": "SSID is busy or under maintenance. Please try again later." }` | + +--- + +### 3. View details of flagged pairwise submissions comparison results + +**URL**: `/api/v1/assignments/{assignment_id}/submission_similarities/{submission_similarities_id}` + +**Method**: `GET` + +**Authentication required**: YES + +**Description**: Returns details of a pair of a flagged submissions. Please use SSID's web interface to view and mark students as suspicious or guilty. The web equivalence is going to `/assignments/{assignment_id}/submission_similarities/{submission_similarity_id}`. + +**JSON Parameters**: + +- **No param**. Pass in the desired assignment id and submission similarity id in the URL. + +**Request Example**: +| Header | +| --- | + +``` +{ + "Content-Type": "application/json", + "X-Api-Key": "YOUR_API_KEY" +} +``` + +| Body | +| ---- | + +``` + +``` + +**Possible responses**: + +| Code | Status | Return body | +| ---- | ------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| 200 | Successful | `{ "similarity": ..., "matches": [], "pdf_link": ... }` **[Note: similarity is between 0 and 100]** | +| 400 | Error | `{ “error”: “Submission similarities requested does not exist." }` | +| 401 | Unauthorized | `{ "error": "Missing or invalid API key." }` or `{ "error": "Your API key is not authorized to access this resource." }` | +| 503 | Service Unavailable | `{ "error": "SSID is busy or under maintenance. Please try again later." }` | + +--- + +### 4. View PDF report of flagged pairwise submissions comparison results + +**URL**: `/api/v1/assignments/{assignment_id}/submission_similarities/{submission_similarities_id}/view_pdf` + +**Method**: `GET` + +**Authentication required**: YES + +**Description**: Returns details of a pair of a flagged submissions in PDF. Please use SSID's web interface to view and mark students as suspicious or guilty. The web equivalence is going to `/assignments/{assignment_id}/submission_similarities/{submission_similarity_id}`. + +**JSON Parameters**: + +- **No param**. Pass in the desired assignment id and submission similarity id in the URL. + +**Request Example**: +| Header | +| --- | + +``` +{ + "Content-Type": "application/json", + "X-Api-Key": "YOUR_API_KEY" +} +``` + +| Body | +| ---- | + +``` + +``` + +**Possible responses**: + +| Code | Status | Return body | +| ---- | ------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| 200 | Successful | The generated PDF Report **[Note: similarity is between 0 and 100]** | +| 400 | Error | `{ “error”: “Submission similarities requested does not exist." }` | +| 401 | Unauthorized | `{ "error": "Missing or invalid API key." }` or `{ "error": "Your API key is not authorized to access this resource." }` | +| 503 | Service Unavailable | `{ "error": "SSID is busy or under maintenance. Please try again later." }` | + +--- \ No newline at end of file diff --git a/doc/developer_guide.md b/doc/developer_guide.md new file mode 100644 index 00000000..242defc6 --- /dev/null +++ b/doc/developer_guide.md @@ -0,0 +1,154 @@ +--- +layout: page +title: Developer Guide +--- + +## Table of Contents +- [Acknowledgements](#acknowledgements) +- [Setting up, getting started](#setting-up-getting-started) +- [Git Practices](#git-practices) + - [Branching](#branching) + - [Commit Messages](#commit-messages) + - [Pull Requests](#pull-requests) + - [Merging](#merging) +- [Architecture](#architecture) +- [Implementation](#implementation) +- [Documentation, logging, testing, configuration, dev-ops](#documentation-logging-testing-configuration-dev-ops) +- [Appendix: Requirements](#appendix-requirements) + - [Product scope](#product-scope) + - [User stories](#user-stories) + - [Non-Functional Requirements](#non-functional-requirements) + - [Glossary](#glossary) + + + +--- + +## **Acknowledgements** + +* Libraries used: Ruby on Rails + + +--- + +## **Setting up, getting started** + +Refer to the guide [_Setting up and getting started_](SettingUp.md). + +--- +## **Git Practices** +### **Branching** +We use the [GitFlow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) branching model. This means that we have two main branches: `master` and `develop`. `master` is the branch that is deployed to production, and `develop` is the branch that is deployed to staging. + +When you are working on a new feature, you should create a new branch off of `develop` with the name `feature/`. When you are done with your feature, you should create a pull request to merge your feature branch into `develop`. + +When you are working on a hotfix, you should create a new branch off of `master` with the name `hotfix/`. When you are done with your hotfix, you should create a pull request to merge your hotfix branch into `master`. + +### **Commit Messages** +Commit messages should be written in the imperative mood. This means that they should be written as if you are giving a command. For example, `Add new feature` is a good commit message, but `Added new feature` is not. + +### **Pull Requests** +Pull requests should be reviewed by at least one other developer before being merged. When you create a pull request, you should assign it to the person who will be reviewing it. You should also add the `Needs Review` label to the pull request. When the reviewer is done reviewing the pull request, they should remove the `Needs Review` label and add either the `Approved` or `Changes Requested` label. If the reviewer requests changes, the developer should make the requested changes and then assign the pull request back to the reviewer. If the reviewer approves the pull request, the developer should merge the pull request. + +### **Merging** +When you merge a pull request, you should use the `Squash and Merge` option. This will squash all of the commits in the pull request into a single commit. The commit message for this commit should be the title of the pull request. This commit message should be written in the imperative mood. For example, `Add new feature` is a good commit message, but `Added new feature` is not. + +### **Rebase** +When you are working on a feature branch, you should rebase your branch onto `develop` regularly. This will ensure that your branch is up to date with the latest changes in `develop`. This will also make it easier to merge your branch into `develop` when you are done with your feature. + +### **Squshing relevant commits together for a PR** +When you are working on a feature branch, you should squash your commits into functionally meaningful commits before it is reviewed. This will make it easier to review your pull request. + +--- + + +## **Design** + +### **Architecture** + +This program is created as a standard Ruby on Rails project following the Model Controller View (MCV) architecture. + +### **Security Considerations** + +### **Database Design** + +### **User roles and authorizations** + + + + + +--- + + +## **Documentation, logging, testing, configuration, dev-ops** + +* [Documentation guide](https://ay2122s1-cs2103-w14-1.github.io/tp/Documentation.html) +* [Testing guide](https://ay2122s1-cs2103-w14-1.github.io/tp/Testing.html) +* [Logging guide](https://ay2122s1-cs2103-w14-1.github.io/tp/Logging.html) +* [Configuration guide](https://ay2122s1-cs2103-w14-1.github.io/tp/Configuration.html) +* [DevOps guide](https://ay2122s1-cs2103-w14-1.github.io/tp/DevOps.html) + +--- + +## **Appendix: Requirements** + +### Product scope + +**Target user profile**: + +* Course instructors for computer sciences courses. +* Teaching assistants for computer science courses. +* Course instructors that are concerned about plagiarism within their courses. +* Course instructors teaching computer science courses with large number of students submitting coding assignments. + +**Value proposition**: +* Provide a centralised platform for course instructors to check for plagiarism among student submissions. +* Provide ways for teaching staff to flag out and confirm cases of plagiarism. + + +With the growth of student intake in computing courses, it becomes an increasingly difficult task to ensure the academic integrity of student submissions. As such, an automated form of plagiarism detection for large student bodies is required. Student Submission Integrity Diagnosis (SSID) aims to resolve this issue by providing an easily accessible platform for course instructors to detect the presence of plagiarism. + +### User stories + +Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` + + +| Priority | As a … | I want to … | So that I can… | +| -------- | ---------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| `* * *` | course instructor | create a new account | gain access to SSID services | +| `* * *` | course instructor | login to my account | gain access to SSID services | +| `* * *` | course instructor | create/view/update/delete my courses | manage my courses | +| `* * *` | course instructor | upload student submission data | check student submissions for plagiarism | +| `* * *` | course instructor | delete student submission data | delete sensitive information once it is no longer required | +| `* * *` | course instructor | view the result of plagiarism analysis | be updated on what are the potential cases of plagiarism | +| `* * *` | course instructor | invite teaching assistants to join my course | have more assistants helping me check for student plagiarism | +| `* * *` | course instructor | flag cases of student submissions for potential plagiarism | separate these submissions for more detailed analysis | +| `* * *` | course instructor | confirm cases of student submissions for plagiarism | mark cases of plagiarism as closed | +| `* * *` | teaching assistant | create a new account | gain access to SSID services | +| `* * *` | teaching assistant | login to my account | gain access to SSID services | +| `* * *` | teaching assistant | view the result of plagiarism analysis | be updated on what are the potential cases of plagiarism | +| `* * *` | teaching assistant | flag cases of student submissions for potential plagiarism | separate these submissions for more detailed analysis | +| `* *` | new user | go through a tutorial at the beginning | familiarise myself with how the software works and what it can do | + + +*{More to be added}* + + + +### Non-Functional Requirements + +1. Should work on any _mainstream OS_. +2. Should return the result of student submission analysis of a standard course size in a reasonable timeframe. +3. Should be secure and ensure that all sensitive information is securely stored. +8. Should be easily accessible by non-expert users. + + +### Glossary + +| Term | Definition | +|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------| +| Mainstream OS | Windows, Linux, Unix, OS-X | + + +--- diff --git a/doc/docker.md b/doc/docker.md new file mode 100644 index 00000000..009e114f --- /dev/null +++ b/doc/docker.md @@ -0,0 +1,31 @@ +Set-up guide using Docker +======================= + +This guide will list the steps required to setup the application using Docker. + +### Prerequisites + +Please ensure that you have downloaded Docker in your local machine. Docker can be installed from https://www.docker.com/products/docker-desktop/. When you open the Docker web application, the application should be displaying a green icon on the bottom left with the hover text, "Running". + +### Set-up commands +1. Clone SSID's source code onto your computer +
    git clone https://github.com/WING-NUS/SSID.git
    +2. Copy all the contents from config/database.docker.yml to replace all the contents of config/database.yml. +3. From here on, if any of the commands fail, try adding sudo in front of the commands (for Mac/Linux systems). For Windows systems, try running the commands in an administrator command prompt. +4. Open a terminal and under the application root directory, run the command below: +
    docker build .
    +5. Next run the below commands: +
    cd config/
    +   docker-compose run web bundle install
    +   docker-compose run web rails db:setup
    +   docker-compose build 
    +   docker-compose up
    +6. Go to 127.0.0.1:3000 in the browser to see the login interface +7. Login with username `ssidadmin123@example.com` and password `SSIDPassword123!` + +### Shutting down the application +1. To shut down the application, press Ctrl+C in the terminal where the application is running. + +### Turning on the application +1. To turn on the application, run the command below: +
    docker-compose up
    \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 00000000..8d5b6835 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +# Remove a potentially pre-existing server.pid for Rails. +rm -f /ssid/tmp/pids/server.pid + +exec "$@" \ No newline at end of file diff --git a/lib/api_keys_handler.rb b/lib/api_keys_handler.rb new file mode 100644 index 00000000..f86177e8 --- /dev/null +++ b/lib/api_keys_handler.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# This file is part of SSID. +# +# SSID is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SSID is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with SSID. If not, see . + +require 'zip' +require 'open3' + +module APIKeysHandler + class APIKeyError < StandardError + def initialize(message, status) + super(message) + @status = status + end + attr_reader :status + end + + class << self; attr_accessor :api_key, :course; end + + def self.authenticate_api_key + raise APIKeyError.new('Missing or invalid API key.', :unauthorized) if api_key.nil? || api_key.user_id.nil? + + return if authorized_for_course?(api_key.user_id, course.id) + + raise APIKeyError.new('Your API key is not authorized to access this resource.', :unauthorized) + end + + def self.authorized_for_course?(user_id, course_id) + # Admins are allowed to access all courses + user = User.find(user_id) + if user.is_admin + return true + end + user_course_membership = UserCourseMembership.find_by(user_id: user_id, course_id: course_id) + user_course_membership.present? + end +end diff --git a/spec/api_requests/api_v1_assignments_requests_spec.rb b/spec/api_requests/api_v1_assignments_requests_spec.rb new file mode 100644 index 00000000..db455736 --- /dev/null +++ b/spec/api_requests/api_v1_assignments_requests_spec.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +RSpec.describe 'api v1 assignments requests', type: :request do + describe 'POST /api/v1/courses/:course_id/assignments/' do + context 'successful' do + before do + init_tests + course = Course.find_by(name: 'Introduction to Programming') + post "/api/v1/courses/#{course.id}/assignments/", params: + { + title: 'assignment', + language: 'java', + sizeOfNGram: 5, + studentSubmissions: fixture_file_upload('minimal_student_submissions.zip', + 'application/octet-stream') + }, + headers: { + 'X-API-KEY' => 'SSID_RSPEC_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns an ok status' do + expect(response).to have_http_status(:ok) + end + + it 'returns an assignmentID' do + response.body.should include 'assignmentID' + end + end + + context 'successful with CSV mapping' do + before do + init_tests + course = Course.find_by(name: 'Introduction to Programming') + post "/api/v1/courses/#{course.id}/assignments/", params: + { + title: 'assignment', + language: 'java', + sizeOfNGram: 5, + studentSubmissions: fixture_file_upload('minimal_student_submissions.zip', + 'application/octet-stream'), + mappingFile: fixture_file_upload('minimal_student_submissions_mapping.csv', + 'application/octet-stream') + }, + headers: { + 'X-API-KEY' => 'SSID_RSPEC_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns an ok status' do + expect(response).to have_http_status(:ok) + end + + it 'returns an assignmentID' do + response.body.should include 'assignmentID' + end + end + + context 'with invalid language value' do + before do + init_tests + course = Course.find_by(name: 'Introduction to Programming') + post "/api/v1/courses/#{course.id}/assignments/", params: + { + title: 'assignment', + language: 'cpp17', + sizeOfNGram: 5, + studentSubmissions: fixture_file_upload('minimal_student_submissions.zip', + 'application/octet-stream') + }, + headers: { + 'X-API-KEY' => 'SSID_RSPEC_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns a 400 status' do + expect(response).to have_http_status(:bad_request) + end + + it 'returns correct error message' do + response.body.should include 'Value of language is not valid' + end + end + + context 'with missing title parameter' do + before do + init_tests + course = Course.find_by(name: 'Introduction to Programming') + + post "/api/v1/courses/#{course.id}/assignments/", params: + { + language: 'java', + sizeOfNGram: 5, + studentSubmissions: fixture_file_upload('minimal_student_submissions.zip', + 'application/octet-stream') + }, + headers: { + 'X-API-KEY' => 'SSID_RSPEC_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns a 400 status' do + expect(response).to have_http_status(:bad_request) + end + + it 'returns correct error message' do + response.body.should include "Missing required parameter 'title'" + end + end + + context 'with invalid parameter' do + before do + init_tests + course = Course.find_by(name: 'Introduction to Programming') + + post "/api/v1/courses/#{course.id}/assignments/", params: + { + title: 'assignment', + language: 'java', + sizeOfNGram: 5, + studentSubmissions: fixture_file_upload('minimal_student_submissions.zip', + 'application/octet-stream'), + sensitivity: 100 + }, + headers: { + 'X-API-KEY' => 'SSID_RSPEC_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns a 400 status' do + expect(response).to have_http_status(:bad_request) + end + + it 'returns correct error message' do + response.body.should include 'Parameter sensitivity is invalid or not yet supported.' + end + end + + context 'with missing/ invalid API key' do + before do + init_tests + course = Course.find_by(name: 'Introduction to Programming') + + post "/api/v1/courses/#{course.id}/assignments/", params: + { + title: 'assignment', + language: 'java', + sizeOfNGram: 5, + studentSubmissions: fixture_file_upload('minimal_student_submissions.zip', + 'application/octet-stream') + }, + headers: { + 'X-API-KEY' => 'EVIL_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns a 401 status' do + expect(response).to have_http_status(:unauthorized) + end + + it 'returns correct error message' do + response.body.should include 'Missing or invalid API key.' + end + end + end +end diff --git a/spec/api_requests/api_v1_submission_similarities_spec.rb b/spec/api_requests/api_v1_submission_similarities_spec.rb new file mode 100644 index 00000000..3e42e086 --- /dev/null +++ b/spec/api_requests/api_v1_submission_similarities_spec.rb @@ -0,0 +1,291 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +def init_submisision_similarities_tests + init_tests + # Creates sample assignment and sample plagiarism detection results + assignment = Assignment.new do |assignment| + assignment.course_id = Course.find_by(name: 'Introduction to Programming').id + assignment.title = 'RSpec Assignment' + assignment.language = 'java' + assignment.min_match_length = 2 + assignment.ngram_size = 4 + assignment.upload_log = '' + assignment.mapbox = false + end + + assignment.save + + submission_similarity_process = SubmissionSimilarityProcess.new do |process| + process.assignment_id = assignment.id + process.status = SubmissionSimilarityProcess::STATUS_COMPLETED + end + + submission_similarity_process.save + + submission1 = Submission.new do |submission1| + submission1.assignment_id = assignment.id + submission1.student_id = 999_999_998 + submission1.lines = ['import java.util.*;', '', '/**', ' * Driver class', ' */', '', 'public class CitiesDriver {', + '', ' ', ' public static void main(String[] args) {', "\t\tSystem.out.println(\"test\");", '', ' }', '', '}'] + submission1.is_plagiarism = false + end + + submission1.save + + submission2 = Submission.new do |submission2| + submission2.assignment_id = assignment.id + submission2.student_id = 999_999_999 + submission2.lines = ['import java.util.*;', '', '/**', ' * Driver class', ' */', '', 'public class CitiesDriver {', + '', ' ', ' public static void main(String[] args) {', "\t\tSystem.out.println(\"test\");", "\t\tSystem.out.println(\"test2\");", '', ' }', '', '}'] + submission2.is_plagiarism = false + end + + submission2.save + + submission_similarity = SubmissionSimilarity.new do |submission_similarity| + submission_similarity.assignment_id = assignment.id + submission_similarity.submission1_id = submission1.id + submission_similarity.submission2_id = submission2.id + submission_similarity.similarity_1_to_2 = 0.35751e2 + submission_similarity.similarity_2_to_1 = 0.35257e2 + submission_similarity.similarity = 0.35751e2 + submission_similarity.status = 0 + end + + submission_similarity.save + + submission_similarity_mapping = SubmissionSimilarityMapping.new do |submission_similarity_mapping| + submission_similarity_mapping.submission_similarity_id = submission_similarity.id + submission_similarity_mapping.start_index1 = 602 + submission_similarity_mapping.end_index1 = 852 + submission_similarity_mapping.start_index2 = 681 + submission_similarity_mapping.end_index2 = 878 + submission_similarity_mapping.start_line1 = 28 + submission_similarity_mapping.end_line1 = 41 + submission_similarity_mapping.start_line2 = 28 + submission_similarity_mapping.end_line2 = 37 + submission_similarity_mapping.statement_count = 5 + submission_similarity_mapping.is_plagiarism = true + end + + submission_similarity_mapping.save +end + +RSpec.describe 'api v1 submission_similarities requests index', type: :request do + describe 'GET /api/v1/assignments/:assignment_id/submission_similarities/' do + context 'successful' do + before do + init_submisision_similarities_tests + + course_id = Course.find_by(name: 'Introduction to Programming').id + assignment = Assignment.find_by(course_id: course_id) + get "/api/v1/assignments/#{assignment.id}/submission_similarities/", headers: { + 'X-API-KEY' => 'SSID_RSPEC_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns an ok status' do + expect(response).to have_http_status(:ok) + end + + it 'returns assignment processing status' do + response.body.should include 'status' + end + end + + context 'with missing/ invalid API key' do + before do + init_submisision_similarities_tests + + course_id = Course.find_by(name: 'Introduction to Programming').id + assignment = Assignment.find_by(course_id: course_id) + get "/api/v1/assignments/#{assignment.id}/submission_similarities/", headers: { + 'X-API-KEY' => 'EVIL_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns a 401 status' do + expect(response).to have_http_status(:unauthorized) + end + + it 'returns correct error message' do + response.body.should include 'Missing or invalid API key.' + end + end + end +end + +RSpec.describe 'api v1 submission_similarities requests show', type: :request do + describe 'GET /api/v1/assignments/:assignment_id/submission_similarities/:submission_similarity_id' do + context 'successful' do + before do + init_submisision_similarities_tests + + assignment = Assignment.find_by(title: 'RSpec Assignment') + submission_similarity = assignment.submission_similarities.first + get "/api/v1/assignments/#{assignment.id}/submission_similarities/#{submission_similarity.id}", headers: { + 'X-API-KEY' => 'SSID_RSPEC_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns an ok status' do + expect(response).to have_http_status(:ok) + end + + it 'returns maxSimilaryPercentage and matches' do + response.body.should include 'matches' + response.body.should include 'similarity' + end + end + + context 'submission similarities requested do not exist' do + before do + init_submisision_similarities_tests + course_id = Course.find_by(name: 'Introduction to Programming').id + assignment = Assignment.find_by(course_id: course_id) + + get "/api/v1/assignments/#{assignment.id}/submission_similarities/99999999", headers: { + 'X-API-KEY' => 'SSID_RSPEC_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns a 400 status' do + expect(response).to have_http_status(:bad_request) + end + + it 'returns correct error message' do + response.body.should include 'Submission similarities requested do not exist.' + end + end + + context 'with missing/ invalid API key' do + before do + init_submisision_similarities_tests + + course_id = Course.find_by(name: 'Introduction to Programming').id + assignment = Assignment.find_by(course_id: course_id) + submission_similarity = assignment.submission_similarities.first + get "/api/v1/assignments/#{assignment.id}/submission_similarities/#{submission_similarity.id}", headers: { + 'X-API-KEY' => 'EVIL_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns a 401 status' do + expect(response).to have_http_status(:unauthorized) + end + + it 'returns correct error message' do + response.body.should include 'Missing or invalid API key.' + end + end + end +end + + +RSpec.describe 'api v1 submission_similarities requests view_pdf', type: :request do + describe 'GET /api/v1/assignments/:assignment_id/submission_similarities/:submission_similarity_id/view_pdf' do + context 'successful PDF generation' do + before do + init_submisision_similarities_tests + + assignment = Assignment.find_by(title: 'RSpec Assignment') + @submission_similarity = assignment.submission_similarities.first + get "/api/v1/assignments/#{assignment.id}/submission_similarities/#{@submission_similarity.id}/view_pdf", headers: { + 'X-API-KEY' => 'SSID_RSPEC_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns an ok response' do + expect(response).to have_http_status(:ok) + end + + it 'sets the content type to PDF' do + expect(response.headers['Content-Type']).to eq 'application/pdf' + end + + it 'prompts to download the file' do + expect(response.headers['Content-Disposition']).to include 'attachment' + end + + it 'includes the correct filename' do + expect(response.headers['Content-Disposition']).to include "filename=\"#{@submission_similarity.id}.pdf\"" + end + end + + context 'submission similarities requested do not exist' do + before do + init_submisision_similarities_tests + course_id = Course.find_by(name: 'Introduction to Programming').id + assignment = Assignment.find_by(course_id: course_id) + + get "/api/v1/assignments/#{assignment.id}/submission_similarities/99999999/view_pdf", headers: { + 'X-API-KEY' => 'SSID_RSPEC_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns a 400 status' do + expect(response).to have_http_status(:bad_request) + end + + it 'returns correct error message' do + response.body.should include 'Submission similarities requested do not exist.' + end + end + + context 'with missing/ invalid API key' do + before do + init_submisision_similarities_tests + + course_id = Course.find_by(name: 'Introduction to Programming').id + assignment = Assignment.find_by(course_id: course_id) + submission_similarity = assignment.submission_similarities.first + get "/api/v1/assignments/#{assignment.id}/submission_similarities/#{submission_similarity.id}/view_pdf", headers: { + 'X-API-KEY' => 'EVIL_API_KEY' + } + end + + after do + clear_tests + end + + it 'returns a 401 status' do + expect(response).to have_http_status(:unauthorized) + end + + it 'returns correct error message' do + response.body.should include 'Missing or invalid API key.' + end + end + end +end \ No newline at end of file diff --git a/spec/fixtures/minimal_student_submissions.zip b/spec/fixtures/minimal_student_submissions.zip new file mode 100644 index 00000000..a432387d Binary files /dev/null and b/spec/fixtures/minimal_student_submissions.zip differ diff --git a/spec/fixtures/minimal_student_submissions_mapping.csv b/spec/fixtures/minimal_student_submissions_mapping.csv new file mode 100644 index 00000000..d1651ca7 --- /dev/null +++ b/spec/fixtures/minimal_student_submissions_mapping.csv @@ -0,0 +1,2 @@ +01,Joe +02,Jane \ No newline at end of file diff --git a/spec/landing_page_spec.rb b/spec/landing_page_spec.rb new file mode 100644 index 00000000..0bc8d1a8 --- /dev/null +++ b/spec/landing_page_spec.rb @@ -0,0 +1,63 @@ +require "spec_helper" + +describe "landing page", type: :feature do + before(:all) do + Capybara.current_driver = :selenium_chrome_headless + end + + it "click on Log In" do + visit '/' + click_button "Log In" + expect(page).to have_content 'Remember me' + end +end + +describe "landing page", type: :feature do + before(:all) do + Capybara.current_driver = :selenium_chrome_headless + window = Capybara.current_session.driver.browser.manage.window + window.resize_to(1920, 1080) + end + + it "click on logo" do + visit '/' + click_link "Student Submissions Integrity Diagnosis System" + expect(page).to have_content 'Hear from our instructors' + end +end + +describe "landing page", type: :feature do + before(:all) do + Capybara.current_driver = :selenium_chrome_headless + end + + it "click on Get started" do + visit '/' + click_button "Get started" + expect(page).to have_content '(8 characters minimum)' + end +end + +describe "landing page", type: :feature do + before(:all) do + Capybara.current_driver = :selenium_chrome_headless + end + + it "click on Sign Up" do + visit '/' + click_button "Sign Up" + expect(page).to have_content '(8 characters minimum)' + end +end + +describe "landing page", type: :feature do + before(:all) do + Capybara.current_driver = :selenium_chrome_headless + end + + it "click on Create account" do + visit '/' + click_button "Create account" + expect(page).to have_content '(8 characters minimum)' + end +end \ No newline at end of file diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 00000000..d4006424 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../config/environment', __dir__) +# Prevent database truncation if the environment is production +abort('The Rails environment is running in production mode!') if Rails.env.production? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/routes/api_v1_assignments_post_route_spec.rb b/spec/routes/api_v1_assignments_post_route_spec.rb new file mode 100644 index 00000000..e8ce79a7 --- /dev/null +++ b/spec/routes/api_v1_assignments_post_route_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +# Ensures that the route is correctly set up +describe 'api v1 assignments post route', type: :routing do + it 'routes to api/v1/assignments#create' do + expect(post: 'api/v1/courses/:course_id/assignments').to route_to( + :controller => 'api/v1/assignments', + 'action' => 'create', + 'course_id' => ':course_id' + ) + end +end diff --git a/spec/routes/api_v1_submission_similarities_routes_spec.rb b/spec/routes/api_v1_submission_similarities_routes_spec.rb new file mode 100644 index 00000000..da998533 --- /dev/null +++ b/spec/routes/api_v1_submission_similarities_routes_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +# Ensures that the routes are correctly set up +# GET /api/v1/assignments/{assignment_id}/submission_similarities/ +describe 'api v1 submission_similarities index', type: :routing do + it 'routes to api/v1/submission_similarities#index' do + expect(get: 'api/v1/assignments/:assignment_id/submission_similarities/').to route_to( + :controller => 'api/v1/submission_similarities', + 'action' => 'index', + 'assignment_id' => ':assignment_id' + ) + end +end + +# GET /api/v1/assignments/{assignment_id}/submission_similarities/{submission_similarity_id} +describe 'api v1 submission_similarities show', type: :routing do + it 'routes to api/v1/submission_similarities#show' do + expect(get: 'api/v1/assignments/:assignment_id/submission_similarities/:submission_similarity_id').to route_to( + :controller => 'api/v1/submission_similarities', + 'action' => 'show', + 'assignment_id' => ':assignment_id', + 'id' => ':submission_similarity_id' + ) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..78514d06 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration + +require 'capybara/rspec' +require 'simplecov' +require 'simplecov-lcov' +require 'pdfkit' + +Capybara.register_driver :chrome do |app| + Capybara::Selenium::Driver.new(app, browser: :chrome) +end + +Capybara.register_driver :headless_chrome do |app| + capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( + chromeOptions: { + args: %w[headless enable-features=NetworkService,NetworkServiceInProcess] + } + ) + + Capybara::Selenium::Driver.new app, + browser: :chrome, + desired_capabilities: capabilities +end + +Capybara.default_driver = :headless_chrome +Capybara.javascript_driver = :headless_chrome + +Capybara.configure do |config| + config.app_host = 'http://127.0.0.1:3000/' +end + +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + # config.disable_monkey_patching! + # + # # This setting enables warnings. It's recommended, but in some cases may + # # be too noisy due to issues in dependencies. + # config.warnings = true + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = "doc" + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed +end + +def init_tests + # Creates a sample user + user = User.new do |user| + user.name = 'ssid_api_user' + user.full_name = 'SSID API User' + user.password = 'SSIDPassword123!' + user.password_confirmation = 'SSIDPassword123!' + user.email = 'ssid_api_user@example.com' + user.is_admin = false + user.is_admin_approved = true + user.confirmed_at = Time.zone.now + user.confirmation_sent_at = Time.zone.now + end + user.skip_confirmation! # Used to confirm account when seeding a user to bypass user.confirmed? check + user.save + + # Creates a sample API key + api_key = ApiKey.new do |api_key| + api_key.user_id = User.find_by(name: 'ssid_api_user').id + api_key.value = 'SSID_RSPEC_API_KEY' + end + + api_key.save + + # Creates a sample course + course = Course.new do |course| + course.code = 'CS101' + course.name = 'Introduction to Programming' + course.academic_year = '2022/2023' + course.semester = 1 + course.expiry = '2024-09-23 23:08:00' + end + + course.save + + # Creates a sample UserCourseMembership + membership = UserCourseMembership.new do |membership| + membership.user_id = User.find_by(name: 'ssid_api_user').id + membership.course_id = Course.find_by(name: 'Introduction to Programming').id + membership.role = 0 # teaching staff + end + + membership.save +end + +# Removes all newly-created data +def clear_tests + User.find_by(name: 'ssid_api_user').destroy + course = Course.find_by(name: 'Introduction to Programming') + course.destroy +end diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index d19212ab..33264b9a 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -1,5 +1,5 @@ require "test_helper" class ApplicationSystemTestCase < ActionDispatch::SystemTestCase - driven_by :selenium, using: :chrome, screen_size: [1400, 1400] + driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400] end diff --git a/test/controllers/password_resets_controller_test.rb b/test/controllers/password_resets_controller_test.rb deleted file mode 100644 index 9acfc361..00000000 --- a/test/controllers/password_resets_controller_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'test_helper' - -class PasswordResetsControllerTest < ActionDispatch::IntegrationTest - test "should get forget_password" do - get password_resets_forget_password_url - assert_response :success - end - - test "should get send_password_reset_link_" do - get password_resets_send_password_reset_link__url - assert_response :success - end - - test "should get reset_password" do - get password_resets_reset_password_url - assert_response :success - end - - test "should get update_password" do - get password_resets_update_password_url - assert_response :success - end - -end diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb deleted file mode 100644 index 4207a9c9..00000000 --- a/test/mailers/previews/user_mailer_preview.rb +++ /dev/null @@ -1,11 +0,0 @@ -# Preview all emails at http://localhost:3000/rails/mailers/user_mailer -class UserMailerPreview < ActionMailer::Preview - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/account_activation - def account_activation - user = User.first - user.activation_token = User.new_token - UserMailer.account_activation(user) - end - -end diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb index e916f05d..3f0faceb 100644 --- a/test/mailers/user_mailer_test.rb +++ b/test/mailers/user_mailer_test.rb @@ -1,12 +1,5 @@ require 'test_helper' class UserMailerTest < ActionMailer::TestCase - test "account_activation" do - mail = UserMailer.account_activation - assert_equal "Account activation", mail.subject - assert_equal ["to@example.org"], mail.to - assert_equal ["from@example.com"], mail.from - assert_match "Hi", mail.body.encoded - end end diff --git a/test/test-database/cs1010-subset.zip b/test/test-database/cs1010-subset.zip new file mode 100644 index 00000000..ff102e0c Binary files /dev/null and b/test/test-database/cs1010-subset.zip differ