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 64595555..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' @@ -46,6 +49,8 @@ gem 'jquery-rails', '~> 4.4' gem 'bootstrap', '~> 5.1.3' +gem 'whenever', '~> 1.0' + group :assets do gem 'sass-rails', '~> 6.0' gem 'coffee-rails', '~> 5.0' @@ -58,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 @@ -65,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 411afe44..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 @@ -79,6 +86,7 @@ GEM regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) childprocess (3.0.0) + chronic (0.10.2) coffee-rails (5.0.0) coffee-script (>= 2.2.0) railties (>= 5.2.0) @@ -86,14 +94,42 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.1.8) + 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) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.8.10) + 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) activesupport (>= 5.0.0) @@ -101,7 +137,9 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - loofah (2.18.0) + json (2.6.3) + jwt (2.7.0) + loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -109,22 +147,75 @@ GEM marcel (1.0.1) method_source (1.0.0) mini_mime (1.1.0) - mini_portile2 (2.8.0) - minitest (5.14.4) + 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.6) + nokogiri (1.13.10) mini_portile2 (~> 2.8.0) racc (~> 1.4) - nokogiri (1.13.6-x86_64-linux) + 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.0) - rack (2.2.4) + racc (1.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) @@ -145,17 +236,68 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.3) - loofah (~> 2.3) + rails-html-sanitizer (1.5.0) + loofah (~> 2.19, >= 2.19.1) railties (6.0.3.7) actionpack (= 6.0.3.7) activesupport (= 6.0.3.7) 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) @@ -166,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) @@ -176,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) @@ -186,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) @@ -198,10 +359,12 @@ GEM websocket-driver (0.7.3) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) + whenever (1.0.0) + chronic (>= 0.6.3) will_paginate (3.3.0) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.4.2) + zeitwerk (2.6.6) PLATFORMS ruby @@ -212,27 +375,48 @@ 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 uglifier (~> 4.2) web-console (>= 3.3.0) webdrivers + whenever (~> 1.0) will_paginate (~> 3.3) 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-course-users-01.png b/app/assets/images/add-course-users-01.png new file mode 100644 index 00000000..e45b0d8c Binary files /dev/null and b/app/assets/images/add-course-users-01.png differ diff --git a/app/assets/images/add-course-users-02.png b/app/assets/images/add-course-users-02.png new file mode 100644 index 00000000..1f2e8c46 Binary files /dev/null and b/app/assets/images/add-course-users-02.png differ diff --git a/app/assets/images/add-course-users-03.png b/app/assets/images/add-course-users-03.png new file mode 100644 index 00000000..98c40e60 Binary files /dev/null and b/app/assets/images/add-course-users-03.png differ 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/creating-courses-01.png b/app/assets/images/creating-courses-01.png new file mode 100644 index 00000000..60a548a4 Binary files /dev/null and b/app/assets/images/creating-courses-01.png differ diff --git a/app/assets/images/creating-courses-02.png b/app/assets/images/creating-courses-02.png new file mode 100644 index 00000000..171246d8 Binary files /dev/null and b/app/assets/images/creating-courses-02.png differ 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/p1.png b/app/assets/images/p1.png index 9b5e943c..9c1e82fd 100644 Binary files a/app/assets/images/p1.png and b/app/assets/images/p1.png differ diff --git a/app/assets/images/p10.png b/app/assets/images/p10.png index 7e570244..c99ffc31 100644 Binary files a/app/assets/images/p10.png and b/app/assets/images/p10.png differ diff --git a/app/assets/images/p11.png b/app/assets/images/p11.png index 5a28ea3d..3194663c 100644 Binary files a/app/assets/images/p11.png and b/app/assets/images/p11.png differ diff --git a/app/assets/images/p12.png b/app/assets/images/p12.png index 7faf8434..38bb79b5 100644 Binary files a/app/assets/images/p12.png and b/app/assets/images/p12.png differ diff --git a/app/assets/images/p13.png b/app/assets/images/p13.png index 1f98bd06..1ffcda1f 100644 Binary files a/app/assets/images/p13.png and b/app/assets/images/p13.png differ diff --git a/app/assets/images/p14.png b/app/assets/images/p14.png index a54bc1a3..878b40e0 100644 Binary files a/app/assets/images/p14.png and b/app/assets/images/p14.png differ diff --git a/app/assets/images/p15.png b/app/assets/images/p15.png index 102e616c..ef2de0e5 100644 Binary files a/app/assets/images/p15.png and b/app/assets/images/p15.png differ diff --git a/app/assets/images/p16.png b/app/assets/images/p16.png index 15ded2d3..2c3e4c79 100644 Binary files a/app/assets/images/p16.png and b/app/assets/images/p16.png differ diff --git a/app/assets/images/p17.png b/app/assets/images/p17.png index ac6101a2..a3f43103 100644 Binary files a/app/assets/images/p17.png and b/app/assets/images/p17.png differ diff --git a/app/assets/images/p18.png b/app/assets/images/p18.png index b29d4dd3..c8d1864f 100644 Binary files a/app/assets/images/p18.png and b/app/assets/images/p18.png differ diff --git a/app/assets/images/p19.png b/app/assets/images/p19.png index 41ab9651..608a8bf6 100644 Binary files a/app/assets/images/p19.png and b/app/assets/images/p19.png differ diff --git a/app/assets/images/p2.png b/app/assets/images/p2.png index 1aabd039..e1d33782 100644 Binary files a/app/assets/images/p2.png and b/app/assets/images/p2.png differ diff --git a/app/assets/images/p20.png b/app/assets/images/p20.png index 528ab8e9..c0235162 100644 Binary files a/app/assets/images/p20.png and b/app/assets/images/p20.png differ diff --git a/app/assets/images/p21.png b/app/assets/images/p21.png index 503ccd95..5f049018 100644 Binary files a/app/assets/images/p21.png and b/app/assets/images/p21.png differ diff --git a/app/assets/images/p22.png b/app/assets/images/p22.png index fa12ce05..7cb8dffe 100644 Binary files a/app/assets/images/p22.png and b/app/assets/images/p22.png differ diff --git a/app/assets/images/p3.png b/app/assets/images/p3.png index a8a60acd..c1db2ddc 100644 Binary files a/app/assets/images/p3.png and b/app/assets/images/p3.png differ diff --git a/app/assets/images/p4.png b/app/assets/images/p4.png deleted file mode 100644 index 029411e3..00000000 Binary files a/app/assets/images/p4.png and /dev/null differ diff --git a/app/assets/images/p5.png b/app/assets/images/p5.png index 88c97b25..5acc43bc 100644 Binary files a/app/assets/images/p5.png and b/app/assets/images/p5.png differ diff --git a/app/assets/images/p6.png b/app/assets/images/p6.png index c44a1d5b..d03de845 100644 Binary files a/app/assets/images/p6.png and b/app/assets/images/p6.png differ diff --git a/app/assets/images/p7.png b/app/assets/images/p7.png index 4c8bac80..3f3cf976 100644 Binary files a/app/assets/images/p7.png and b/app/assets/images/p7.png differ diff --git a/app/assets/images/p8.png b/app/assets/images/p8.png index c44a1d5b..2f96ddef 100644 Binary files a/app/assets/images/p8.png and b/app/assets/images/p8.png differ diff --git a/app/assets/images/p9.png b/app/assets/images/p9.png index d90ae61e..9c5defd4 100644 Binary files a/app/assets/images/p9.png and b/app/assets/images/p9.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/account_activations.coffee b/app/assets/javascripts/account_activations.coffee new file mode 100644 index 00000000..24f83d18 --- /dev/null +++ b/app/assets/javascripts/account_activations.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/assignments.js.coffee b/app/assets/javascripts/assignments.js.coffee index 33bb9caf..216a924c 100644 --- a/app/assets/javascripts/assignments.js.coffee +++ b/app/assets/javascripts/assignments.js.coffee @@ -17,16 +17,21 @@ along with SSID. If not, see . window.Assignment ||= {} -Assignment.onLoad = -> - $ -> - $("table a").each -> - # Site.registerHighlightRowMethodsForLink(this) - # return - return - +Assignment.updateAdvancedOptions = -> + $("#assignment_used_fingerprints").click (event) -> + used_fingerprints = $('input[name="assignment[used_fingerprints]"]:checked'); + if used_fingerprints.length > 0 + $('input[name="assignment[ngram_size]"]').val(5); + else + $('input[name="assignment[ngram_size]"]').val(4); + return + $(document).ready -> - if $("#status").length > 0 - $("#status").load statusURL + $ -> + if $("#status").length > 0 + $("#status").load statusURL + Assignment.updateAdvancedOptions() + return # setInterval (-> # $("#status").load statusURL # ), 3000 diff --git a/app/assets/javascripts/assignments_file_upload.js b/app/assets/javascripts/assignments_file_upload.js index 55364897..8b64d3f6 100644 --- a/app/assets/javascripts/assignments_file_upload.js +++ b/app/assets/javascripts/assignments_file_upload.js @@ -54,6 +54,17 @@ along with SSID. If not, see . $("#student_submissions_zip_files_list_label").text("Files inside:"); } + // Change selected language to the appropriate one based on file extension + if (fileNames.length > 0) { + var fileExtension = fileNames[0].split('.').pop(); + var language = Assignment.getLanguageFromExtension(fileExtension); + if (language != null) { + console.log("Changing language to " + language); + console.log("prev language " + $("#student_submissions_language_list").val()); + $("#student_submissions_language_list").val(language); + } + } + // Display the first ten files for (let i = 0; i < Math.min(10, fileNames.length); i++) { let fileName = fileNames[i]; @@ -90,6 +101,26 @@ along with SSID. If not, see . reader.readAsArrayBuffer(fileInput.files[0]); } + + // This hashmap is used to map file extensions to the appropriate language + languageHashMap = { + "java": "java", + "py": "python3", + "c": "c", + "cpp": "cpp", + "js": "javascript", + "r": "r", + "ocaml": "ocaml", + "matlab": "matlab", + "scala": "scala" + } + + // This function returns the language based on the file extension + Assignment.getLanguageFromExtension = function (fileExtension) { + return languageHashMap[fileExtension]; + } + + Assignment.onAssignmentMapFileInputChange = function (fileInput) { if (fileInput.files[0] == undefined) { return; 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 c2775aad..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; @@ -45,56 +68,61 @@ along with SSID. If not, see . $(checkBox).closest("tr").addClass("highlight"); // differentiate colors - let skeletonLines = $(checkBox).closest("input.skeleton-lines").val().split(/\s/) - let skeletonIdxS1 = [] - let skeletonIdxS2 = [] - for (i = startLine1; i <= endLine1; ++i) { - skeletonIdxS1.push(false); - } - - for (i = startLine2; i <= endLine2; ++i) { - skeletonIdxS2.push(false); - } - - skeletonLines.forEach((skeletonLine) => { - lineInfo = skeletonLine.split("_") - skeletonStartLine1 = parseInt(lineInfo[0]) - skeletonEndLine1 = parseInt(lineInfo[1]) - skeletonStartLine2 = parseInt(lineInfo[2]) - skeletonEndLine2 = parseInt(lineInfo[3]) - - for (i = 0; i < skeletonIdxS1.length; ++i) { - if (skeletonStartLine1 <= i+startLine1 && i+startLine1 <= skeletonEndLine1) { - skeletonIdxS1[i] = true; - } + if ($(checkBox).closest("input.skeleton-lines").val() != null) { + let skeletonLines = $(checkBox).closest("input.skeleton-lines").val().split(/\s/) + let skeletonIdxS1 = [] + let skeletonIdxS2 = [] + for (i = startLine1; i <= endLine1; ++i) { + skeletonIdxS1.push(false); } - for (i = 0; i < skeletonIdxS2.length; ++i) { - if (skeletonStartLine2 <= i+startLine2 && i+startLine2 <= skeletonEndLine2) { - skeletonIdxS2[i] = true; + for (i = startLine2; i <= endLine2; ++i) { + skeletonIdxS2.push(false); + } + + skeletonLines.forEach((skeletonLine) => { + lineInfo = skeletonLine.split("_") + skeletonStartLine1 = parseInt(lineInfo[0]) + skeletonEndLine1 = parseInt(lineInfo[1]) + skeletonStartLine2 = parseInt(lineInfo[2]) + skeletonEndLine2 = parseInt(lineInfo[3]) + + for (i = 0; i < skeletonIdxS1.length; ++i) { + if (skeletonStartLine1 <= i+startLine1 && i+startLine1 <= skeletonEndLine1) { + skeletonIdxS1[i] = true; + } } - } - }) - - - + for (i = 0; i < skeletonIdxS2.length; ++i) { + if (skeletonStartLine2 <= i+startLine2 && i+startLine2 <= skeletonEndLine2) { + skeletonIdxS2[i] = true; + } + } + }) + for (lineIdx = _i = startLine1; startLine1 <= endLine1 ? _i <= endLine1 : _i >= endLine1; lineIdx = startLine1 <= endLine1 ? ++_i : --_i) { + if (skeletonIdxS1[lineIdx - startLine1] == false) { + $("div.submission1 li:eq(" + lineIdx + ")").addClass("highlight"); + } else { + $("div.submission1 li:eq(" + lineIdx + ")").addClass("skeleton-highlight"); + } + } - for (lineIdx = _i = startLine1; startLine1 <= endLine1 ? _i <= endLine1 : _i >= endLine1; lineIdx = startLine1 <= endLine1 ? ++_i : --_i) { - if (skeletonIdxS1[lineIdx - startLine1] == false) { + for (lineIdx = _j = startLine2; startLine2 <= endLine2 ? _j <= endLine2 : _j >= endLine2; lineIdx = startLine2 <= endLine2 ? ++_j : --_j) { + if (skeletonIdxS2[lineIdx - startLine2] == false) { + $("div.submission2 li:eq(" + lineIdx + ")").addClass("highlight"); + } else { + $("div.submission2 li:eq(" + lineIdx + ")").addClass("skeleton-highlight"); + } + } + } else { + for (lineIdx = _i = startLine1; startLine1 <= endLine1 ? _i <= endLine1 : _i >= endLine1; lineIdx = startLine1 <= endLine1 ? ++_i : --_i) { $("div.submission1 li:eq(" + lineIdx + ")").addClass("highlight"); - } else { - $("div.submission1 li:eq(" + lineIdx + ")").addClass("skeleton-highlight"); } - } - for (lineIdx = _j = startLine2; startLine2 <= endLine2 ? _j <= endLine2 : _j >= endLine2; lineIdx = startLine2 <= endLine2 ? ++_j : --_j) { - if (skeletonIdxS2[lineIdx - startLine2] == false) { + for (lineIdx = _j = startLine2; startLine2 <= endLine2 ? _j <= endLine2 : _j >= endLine2; lineIdx = startLine2 <= endLine2 ? ++_j : --_j) { $("div.submission2 li:eq(" + lineIdx + ")").addClass("highlight"); - } else { - $("div.submission2 li:eq(" + lineIdx + ")").addClass("skeleton-highlight"); - } + } } }; @@ -121,7 +149,7 @@ along with SSID. If not, see . SubmissionSimilarity.toggleRowHighlight = function(el) { var inputNode; - inputNode = $(el).find("input.checkbox"); + inputNode = $(el).find("input"); if (inputNode.is(":checked")) { inputNode.prop("checked", false); SubmissionSimilarity.unhighlightLines(inputNode); @@ -164,34 +192,14 @@ along with SSID. If not, see . Site.registerHighlightRowMethodsForLink(this); }); prettyPrint(); - - $("table.lines td").hover(function() { - SubmissionSimilarity.slideToLine($(this).parent().find("input")); - SubmissionSimilarity.highlightLines($(this).parent().find("input")); - }, function() { - SubmissionSimilarity.unhighlightLines($(this).parent().find("input")); - }); - - $("table.lines th.check_box_col").hover( - function() {$("table.lines tbody tr").each(function() { - var inputNode; - inputNode = $(this).find("input.checkbox"); - if (!inputNode.is(":checked")) { - SubmissionSimilarity.highlightLines(inputNode); - } - }); - }, - function() {$("table.lines tbody tr").each(function() { - var inputNode; - inputNode = $(this).find("input.checkbox"); - if (!inputNode.is(":checked")) { - SubmissionSimilarity.unhighlightLines(inputNode); - } - }); - }); - $("table.lines td.check_box_col").on("click", function() { - SubmissionSimilarity.toggleRowHighlight(this); + $("table.lines td").find("input.checkbox").on("click", function() { + if ($(this).is(":checked")) { + SubmissionSimilarity.slideToLine($(this).closest("tr").find("input")); + SubmissionSimilarity.highlightLines($(this).closest("tr").find("input")); + } else { + SubmissionSimilarity.unhighlightLines($(this).closest("tr").find("input")); + } }); $("table.lines th.check_box_col input").on('click', function(event) { SubmissionSimilarity.toggleAllRowHighlights(this); @@ -201,4 +209,5 @@ along with SSID. If not, see . }); }); }; - }).call(this); \ No newline at end of file + }).call(this); + \ No newline at end of file diff --git a/app/assets/javascripts/visualize.js.coffee b/app/assets/javascripts/visualize.js.coffee index b4ff6a0e..75d5e1d4 100644 --- a/app/assets/javascripts/visualize.js.coffee +++ b/app/assets/javascripts/visualize.js.coffee @@ -446,7 +446,7 @@ VisualizeSimilarityClusterGraph.selectAssignmentForSubmissions = (el) -> $.getJSON "../assignments/"+assignment_id+"/list", (data) -> options = [""] for submission in data - options.push "" + options.push "" $("#submissions").html(options.join("")) $(el).nextAll("span").first().html("(selected 0 of "+(options.length-1)+")") $(el).nextAll("select").first().removeAttr("disabled") @@ -461,7 +461,7 @@ VisualizeTopSimilarSubmissions.selectAssignmentForSubmissions = (el) -> $.getJSON "../../../assignments/"+assignment_id+"/submissions", (data) -> options = [""] for submission in data - options.push "" + options.push "" $("#submissions").html(options.join("")) $(el).nextAll("span").first().html("(selected 0 of "+(options.length-1)+")") $(el).nextAll("select").first().removeAttr("disabled") @@ -482,7 +482,7 @@ VisualizeSimilarityClusterGraph.selectAssignmentForExistingClusters = (el) -> $.getJSON url, (data) -> options = [""] for student in data - options.push "" + options.push "" $("#existing_cluster_submissions").html(options.join("")) $(el).nextAll("span").first().html("(selected 0 of "+(options.length-1)+")") $(el).nextAll("select").first().removeAttr("disabled") diff --git a/app/assets/stylesheets/account_activations.scss b/app/assets/stylesheets/account_activations.scss new file mode 100644 index 00000000..65f3ebe0 --- /dev/null +++ b/app/assets/stylesheets/account_activations.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the AccountActivations controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ 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/assignments.scss b/app/assets/stylesheets/assignments.scss index 4f09850f..cd0f91ff 100644 --- a/app/assets/stylesheets/assignments.scss +++ b/app/assets/stylesheets/assignments.scss @@ -87,7 +87,7 @@ body.assignments { } body.assignments_new, body.assignments_create, body.assignments_show { - form input#assignment_mapbox { + form input#assignment_mapbox, form input#assignment_used_fingerprints { margin-top: $spacer * 0.75; margin-left: $spacer; } 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 7b335653..e102895a 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: unquote("min(600px, 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: unquote("min(600px, 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,13 +249,13 @@ table { padding: 1rem 0.5rem; font-size: 2rem; font-weight: 400; - color: #003d7c; + color: $accent-color; } #menu { @extend .order-1; @extend .ms-md-auto; - padding: 0.5rem; + padding: 0rem; @extend .navbar-nav; flex-direction: row; @extend .flex-wrap; @@ -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; } } @@ -226,3 +302,704 @@ ol.breadcrumb { } } } + +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/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index dac158c5..a04caba2 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -16,199 +16,36 @@ =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) + @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 - 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(id_string: params[:user]["id_string"]).first - if @existing_user - @the_user = @existing_user - else - @the_user.full_name = params[:user]["full_name"] - @the_user.id_string = params[:user]["id_string"] - @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.id_string = 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]["id_string"] - 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 - if @existing_user or not @course.nil? - redirect_to course_users_url(@course), notice: "User was successfully added - to #{@course.code}." - else - redirect_to admin_users_url, notice: 'User was successfully created.' - 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 + @loners = User.all - User.joins(:memberships) - @admins - @signups + end + + def approve + @the_user = User.find(params[:user_id]) + @the_user.update_attribute(:is_admin_approved, true) + @the_user.save + UserMailer.admin_approved(@the_user).deliver_now + 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.id_string = params[:user]["id_string"] - @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 @@ -241,10 +78,19 @@ def destroy end else if @the_user.destroy - redirect_to url, notice: 'User was successfully deleted.' + redirect_to admin_users_url, notice: 'User was successfully deleted.' + else + redirect_to admin_users_url, alert: @the_user.errors.to_a.join(", ") end - redirect_to url, alert: @the_user.errors.to_a.join(", ") 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/assignments_controller.rb b/app/controllers/assignments_controller.rb index e7573974..bf9dc9c9 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -31,7 +31,7 @@ class AssignmentsController < ApplicationController @course = Course.find(params[:course_id]) controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_TEACHING_ASSISTANT, course: @course, - only: [ :index, :cluster_students, :new, :create, :edit, :update] + only: [ :index, :cluster_students, :new, :create, :show, :update] controller.send :authenticate_actions_for_role, UserCourseMembership::ROLE_STUDENT, course: @course, only: [ ] @@ -63,7 +63,7 @@ def cluster_students respond_to do |format| format.json { render json: @assignment.cluster_students.collect { |s| - { id: s.id, id_string: s.id_string } + { id: s.id, name: s.name } } } end @@ -94,6 +94,8 @@ def create a.course_id = @course.id } + puts "DEBUG 06: Enable fingerprints checkbox?" + puts "Checkbox: #{params[:assignment]["used_fingerprints"]}" # Process file if @assignment is valid and file was uploaded if @assignment.valid? @@ -101,6 +103,7 @@ def create return render action: "new" unless @assignment.save isMapEnabled = (params[:assignment]["mapbox"] == "Yes")? true : false; + used_fingerprints = (params[:assignment]["used_fingerprints"] == "Yes")? true : false # No student submission file was uploaded if params[:assignment]["file"].nil? @@ -110,7 +113,7 @@ def create elsif (is_valid_zip?(params[:assignment]["file"].content_type, params[:assignment]["file"].path)) # Don't process the file and show error if the mapping was enabled but no mapping file was uploaded if (is_valid_map_or_no_map?(isMapEnabled, params[:assignment]["mapfile"])) - self.start_upload(@assignment, params[:assignment]["file"], isMapEnabled, params[:assignment]["mapfile"]) + self.start_upload(@assignment, params[:assignment]["file"], isMapEnabled, params[:assignment]["mapfile"], used_fingerprints) # Don't process the file and show error if the mapping was enabled but no mapping file was uploaded elsif (isMapEnabled && params[:assignment]["mapfile"].nil?) @assignment.errors.add(:mapfile, "containing mapped student names need to be uploaded if the 'Upload map file' box is ticked") @@ -140,6 +143,7 @@ def update @assignment = Assignment.find(params[:id]) isMapEnabled = (params[:assignment]["mapbox"] == "Yes")? true : false; + used_fingerprints = (params[:assignment]["used_fingerprints"] == "Yes")? true : false # No student submission file was uploaded if params[:assignment]["file"].nil? @@ -153,7 +157,7 @@ def update elsif (is_valid_zip?(params[:assignment]["file"].content_type, params[:assignment]["file"].path)) # Don't process the file and show error if the mapping was enabled but no mapping file was uploaded if (is_valid_map_or_no_map?(isMapEnabled, params[:assignment]["mapfile"])) - self.start_upload(@assignment, params[:assignment]["file"], isMapEnabled, params[:assignment]["mapfile"]) + self.start_upload(@assignment, params[:assignment]["file"], isMapEnabled, params[:assignment]["mapfile"], used_fingerprints) # Don't process the file and show error if the mapping was enabled but no mapping file was uploaded elsif (isMapEnabled && params[:assignment]["mapfile"].nil?) @assignment.errors.add(:mapfile, "containing mapped student names need to be uploaded if the 'Upload map file' box is ticked") @@ -183,15 +187,21 @@ def destroy redirect_to course_assignments_url(@course), notice: 'Assignment was successfully deleted.' end - def start_upload(assignment, submissionFile, isMapEnabled, mapFile) + def start_upload(assignment, submissionFile, isMapEnabled, mapFile, used_fingerprints) require 'submissions_handler' # Process upload file submissions_path = SubmissionsHandler.process_upload(submissionFile, isMapEnabled, mapFile, assignment) if submissions_path # Launch java program to process submissions - SubmissionsHandler.process_submissions(submissions_path, assignment, isMapEnabled) - redirect_to course_assignments_url(@course), notice: 'SSID will start to process the assignment now. Please refresh this page after a few minutes to view the similarity results.' + SubmissionsHandler.process_submissions(submissions_path, assignment, isMapEnabled, used_fingerprints) + + process = assignment.submission_similarity_process + notice = 'SSID will start to process the assignment now. Please refresh this page after a few minutes to view the similarity results.' + if process && process.status == SubmissionSimilarityProcess::STATUS_WAITING + notice = 'Your assignment has been put into a waiting list. SSID will process it soon. Thank you for your patience.' + end + redirect_to course_assignments_url(@course), notice: notice 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." return render action: "show" diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index d6501c36..955121bd 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -17,7 +17,7 @@ class CoursesController < ApplicationController before_action { |controller| - controller.send :authenticate_actions_for_admin, only: [ :new, :create, :edit, :update, :destroy ] + controller.send :authenticate_actions_for_admin, only: [ :destroy ] } before_action { |controller| if params[:course_id] @@ -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 @@ -72,11 +72,23 @@ def create expiry_date = expiry_time.to_date 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 - redirect_to courses_url, notice: 'Course was successfully created.' + + if @course.valid? and @course.save else render action: "new" + return + end + + @membership = UserCourseMembership.new { |m| + m.user = current_user + m.course = @course + m.role = UserCourseMembership::ROLE_TEACHING_STAFF + } + + if @membership.valid? && @membership.save + redirect_to courses_url, notice: 'Course was successfully created.' + else + render action: 'new' end end @@ -117,9 +129,11 @@ def cluster_students respond_to do |format| format.json { render json: @course.cluster_students.collect { |s| - { id: s.id, id_string: s.id_string } + { id: s.id, name: s.name } } } 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 ab3639a5..00000000 --- a/app/controllers/sessions_controller.rb +++ /dev/null @@ -1,56 +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]) - session[:user_id] = user.id - redirect_to root_url - else - redirect_to login_url, alert: "Invalid user/password combination" - 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 c75365f1..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 @@ -68,8 +80,11 @@ def create_guest_user @guest_user = User.new { |u| u.name = hash_string u.full_name = hash_string - u.id_string = hash_string u.password_digest = BCrypt::Password.create("password") + u.email = "#{hash_string}@ssid.example.com" + u.is_admin_approved = 1 + u.activated = 1 + u.activated_at = Time.zone.now } # create a entry under other tables in database @@ -102,6 +117,20 @@ def create_guest_user end end + # GET /assignments/1/submission_similarities/1/view_printable + def view_printable + @submission_similarity = SubmissionSimilarity.find(params["submission_similarity_id"]) + + render partial: "pair_report" + end + + # GET /assignments/1/view_printable_multiple?submission_similarity_ids=1,2,3 + def view_printable_multiple + @submission_similarity_ids = params["submission_similarity_ids"].split(',') + + render partial: "pair_report_multiple" + end + # GET /assignments/1/submission_similarities/1 def show @submission_similarity = SubmissionSimilarity.find(params["id"]) @@ -132,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 @@ -155,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 @@ -178,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 @@ -200,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 e820b11f..2d2d7f00 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -40,7 +40,7 @@ def index respond_to do |format| format.json { render json: @submissions.collect { |s| - { id: s.id, student_id: s.student.id, student_id_string: s.student_id_string } + { id: s.id, student_id: s.student.id, student_name: s.student_name } } } end @@ -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 687b2526..00000000 --- a/app/controllers/users_controller.rb +++ /dev/null @@ -1,79 +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 - 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 /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 -end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 30f2712f..dee92325 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,12 +1,26 @@ module ApplicationHelper - # mailer constants SSID_MAINTAINER = "The SSID Team" SSID_MAINTAINER_EMAIL = "wing.nus@gmail.com" 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" -end \ No newline at end of file + def self.is_application_healthy + check_memory = `free` + + # Mem: total used free shared buff/cache available + if check_memory + memory_stats = check_memory.split("\n")[1].split("\s") + puts "Output: #{check_memory}" + puts "Memory: #{memory_stats}" + total_memory, available_memory = Integer(memory_stats[1]), Integer(memory_stats[6]) + return available_memory * 100.0 / total_memory > Rails.application.config.critical_available_memory + else + return false + end + end + +end 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 76d3c0c8..19067a81 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -1,18 +1,8 @@ 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.") + + def admin_approved(user) + @user = user + mail(to: @user.email, subject: "[SSID] Your account has been approved.") end end 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/assignment.rb b/app/models/assignment.rb index f069f8ba..101c8948 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -24,6 +24,7 @@ class Assignment < ActiveRecord::Base has_many :confirmed_plagiarism_cases, -> { where("status = #{SubmissionSimilarity::STATUS_CONFIRMED_AS_PLAGIARISM}") }, class_name: "SubmissionSimilarity" has_many :submissions belongs_to :course + attr_accessor :used_fingerprints validates :title, :language, :min_match_length, :ngram_size, presence: true validates_numericality_of :min_match_length, only_integer: true, greater_than: 0 @@ -79,9 +80,11 @@ def submission_clusters }.flatten end - def submission_similarities_for_student(student) - self.submission_similarities.select { |ss| - ss.submission1.student == student or ss.submission2.student == student - } + def submission_similarities_for_student(student, num_display) + the_submission = self.submissions.find_by_student_id(student.id) + + if the_submission + self.submission_similarities.where(["submission1_id = ? or submission2_id = ?", the_submission.id, the_submission.id]).order('assignment_id, similarity DESC').limit(num_display) + end 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/submission.rb b/app/models/submission.rb index c8c15027..73575f6f 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -51,7 +51,7 @@ def lines attr end - def student_id_string - self.student.id_string + def student_name + self.student.name end end diff --git a/app/models/submission_cluster.rb b/app/models/submission_cluster.rb index ad37e851..a77d65c6 100644 --- a/app/models/submission_cluster.rb +++ b/app/models/submission_cluster.rb @@ -22,7 +22,7 @@ class SubmissionCluster < ActiveRecord::Base def submission_student_ids self.submissions.collect { |s| - s.student.id_string + s.student.name } end diff --git a/app/models/submission_log.rb b/app/models/submission_log.rb index c1924ae5..03018b94 100644 --- a/app/models/submission_log.rb +++ b/app/models/submission_log.rb @@ -35,6 +35,6 @@ class SubmissionLog < ActiveRecord::Base belongs_to :submission_similarity def log_string - TYPE_TEMPLATE_STRINGS[self.log_type] + self.submission_similarity.other_student(self.submission.student).id_string + TYPE_TEMPLATE_STRINGS[self.log_type] + self.submission_similarity.other_student(self.submission.student).name end end diff --git a/app/models/submission_similarity_process.rb b/app/models/submission_similarity_process.rb index 656bed47..255cf5f6 100644 --- a/app/models/submission_similarity_process.rb +++ b/app/models/submission_similarity_process.rb @@ -20,6 +20,7 @@ class SubmissionSimilarityProcess < ActiveRecord::Base STATUS_ERRONEOUS = 0 STATUS_RUNNING = 1 STATUS_COMPLETED = 2 + STATUS_WAITING = 3 STATUS_STRINGS = %w{erroneous running completed} belongs_to :assignment diff --git a/app/models/user.rb b/app/models/user.rb index d426bbc0..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,14 +28,13 @@ 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, :id_string, uniqueness: true - validates :id_string, presence: true, if: -> {is_admin == false} - validates :email, 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 def is_some_staff? @@ -46,6 +50,26 @@ def full_name the_full_name.strip.empty? ? nil : the_full_name end + 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 def ensure_an_admin_remains @@ -56,4 +80,13 @@ def ensure_an_admin_remains true end 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])(?=.*?[#?!@$%^&*-])/ + + 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 edee1828..00000000 --- a/app/views/admin/users/_edit_form.html.erb +++ /dev/null @@ -1,50 +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]" %> - <%= f.label :id_string, "ID", class: "form-label" %> - <%= f.text_field :id_string, class: "form-control" %> -
- <% 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, "Login", 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 b9deb0ca..00000000 --- a/app/views/admin/users/_form.html.erb +++ /dev/null @@ -1,60 +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, 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 :id_string, "ID", class: "form-label" %> - <%= f.text_field :id_string, class: "form-control" %> -
-
- <%= 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, "Login", 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 4e9f3ec3..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 %> @@ -63,7 +65,6 @@ - @@ -72,17 +73,62 @@ - + + <% } %> + +
    Full Name/ Hash Login/ HashID/ Hash Actions
    <%= user.full_name || "--" %> <%= user.name %><%= user.id_string %> +
    +
    +<% end %> + +<% unless @signups.empty? %> +
    + +
    New User Signups
    + + + + + + + + + + + <% @signups.each { |user| %> + + + + 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 .
    Full Name/ HashLogin/ HashActions
    <%= user.full_name || "--" %><%= user.name %> +
    <%= @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 %>