Commit 01ce4156 authored by Taico Aerts's avatar Taico Aerts
Browse files

Merge branch 'development' into 'master'

Project Forum Release v2.8.8 - 15-09-2022

See merge request !865
parents 9ce3e5af 0f9d4393
......@@ -9,20 +9,11 @@ stages:
# Include templates
include:
- template: Security/License-Scanning.gitlab-ci.yml
- template: Dependency-Scanning.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
variables:
SAST_EXCLUDED_ANALYZERS: 'brakeman, bundler-audit, semgrep'
license_scanning:
variables:
USE_FREEDESKTOP_PLACEHOLDER: "true"
before_script:
- apt-get update || true
- apt-get install --no-install-recommends -y dpkg-dev file g++ gcc imagemagick libbz2-dev libc6-dev libdb-dev libevent-dev libffi-dev libgdbm-dev libglib2.0-dev libgmp-dev libjpeg-dev libkrb5-dev liblzma-dev libmagickcore-dev libmagickwand-dev libmaxminddb-dev libpng-dev libpq-dev libreadline-dev libtool libwebp-dev libxml2-dev libxslt-dev libyaml-dev patch xz-utils zlib1g-dev libz-dev libiconv-hook1 libgconf-2-4 build-essential libsqlite3-dev libmariadb-dev libglpk-dev nodejs
# -------------------------------------------------------------------------------------------------
# Build base docker image
......@@ -75,30 +66,34 @@ build-branch:
services:
- name: docker.elastic.co/elasticsearch/elasticsearch:7.17.1
alias: elasticsearch
command: [ "bin/elasticsearch", "-Ediscovery.type=single-node" ]
command:
- /bin/bash
- -c
- echo -Xms256m >> /usr/share/elasticsearch/config/jvm.options && echo -Xmx512m >> /usr/share/elasticsearch/config/jvm.options && /usr/local/bin/docker-entrypoint.sh elasticsearch -Ediscovery.type=single-node
- name: mariadb:10
alias: mariadb
variables:
DB_TYPE: sqlite
RAILS_ENV: test
ELASTICSEARCH_URL: "http://elasticsearch:9200"
COVERAGE: "true"
MYSQL_DATABASE: projectforum
MYSQL_ROOT_PASSWORD: root
before_script:
- bundle config --local path /projectforum/vendor
- cp config/database.ci.yml config/database.yml
- mkdir /pf_tests
- cp -r . /pf_tests
script:
- cd /pf_tests
# Check elasticsearch is running
- sleep 10
- 'for i in $(seq 1 10); do curl "$ELASTICSEARCH_URL/_cat/health" && s=0 && break || s=$? && sleep 5; done; (exit $s)'
- 'for i in $(seq 1 12); do curl "$ELASTICSEARCH_URL/_cat/health" && s=0 && break || s=$? && sleep 5; done; (exit $s)'
# Setup environment
- bin/rails db:environment:set
- DB_TYPE=sqlite bundle exec rake db:schema:load
- DB_TYPE=sqlite bundle exec rake db:seed
- bundle exec rails db:schema:load db:seed
# Run the tests
- bundle exec rake test
- bundle exec rails test
after_script:
- cd /pf_tests
- bundle exec rake test:combine
- bundle exec rails test:combine
# Move the output back into the builds folder
- cp test/reports -r --parents $CI_PROJECT_DIR
- cp coverage -r --parents $CI_PROJECT_DIR
......@@ -117,31 +112,35 @@ build-branch:
services:
- name: docker.elastic.co/elasticsearch/elasticsearch:7.17.1
alias: elasticsearch
command: [ "bin/elasticsearch", "-Ediscovery.type=single-node" ]
command:
- /bin/bash
- -c
- echo -Xms256m >> /usr/share/elasticsearch/config/jvm.options && echo -Xmx512m >> /usr/share/elasticsearch/config/jvm.options && /usr/local/bin/docker-entrypoint.sh elasticsearch -Ediscovery.type=single-node
- name: mariadb:10
alias: mariadb
variables:
DB_TYPE: sqlite
RAILS_ENV: test
ELASTICSEARCH_URL: "http://elasticsearch:9200"
CHROME_HEADLESS: "true"
COVERAGE: "true"
MYSQL_DATABASE: projectforum
MYSQL_ROOT_PASSWORD: root
before_script:
- bundle config --local path /projectforum/vendor
- cp config/database.ci.yml config/database.yml
- mkdir /pf_tests
- cp -r . /pf_tests
script:
- cd /pf_tests
# Check elasticsearch is running
- sleep 10
- 'for i in $(seq 1 10); do curl "$ELASTICSEARCH_URL/_cat/health" && s=0 && break || s=$? && sleep 5; done; (exit $s)'
- 'for i in $(seq 1 12); do curl "$ELASTICSEARCH_URL/_cat/health" && s=0 && break || s=$? && sleep 5; done; (exit $s)'
# Setup environment
- bin/rails db:environment:set
- DB_TYPE=sqlite bundle exec rake db:schema:load
- DB_TYPE=sqlite bundle exec rake db:seed
- bundle exec rails db:schema:load db:seed
# Run the system tests
- bundle exec rake test:system
- bundle exec rails test:system
after_script:
- cd /pf_tests
- bundle exec rake test:combine
- bundle exec rails test:combine
# Move the output back into the builds folder
- cp test/reports -r --parents $CI_PROJECT_DIR
- cp coverage -r --parents $CI_PROJECT_DIR
......@@ -160,8 +159,6 @@ build-branch:
'Lint (rubocop)':
stage: test
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
before_script:
- bundle config --local path /projectforum/vendor
script:
- bundle exec rubocop --format progress --format junit --out rubocop.xml --display-only-failed
artifacts:
......@@ -177,8 +174,6 @@ build-branch:
variables:
# brakeman complains when default (POSIX in OS, en_US.ASCII in ruby) is used
LANG: C.UTF-8
before_script:
- bundle config --local path /projectforum/vendor
script:
- bundle exec brakeman --no-progress
......@@ -195,8 +190,6 @@ build-branch:
- if: $CI_COMMIT_BRANCH != "master"
allow_failure: true
- when: on_success
before_script:
- bundle config --local path /projectforum/vendor
script:
- bundle exec bundle-audit update
- bundle exec bundle-audit -v
......@@ -207,21 +200,22 @@ build-branch:
services:
- name: docker.elastic.co/elasticsearch/elasticsearch:7.17.1
alias: elasticsearch
command: [ "bin/elasticsearch", "-Ediscovery.type=single-node" ]
command:
- /bin/bash
- -c
- echo -Xms256m >> /usr/share/elasticsearch/config/jvm.options && echo -Xmx512m >> /usr/share/elasticsearch/config/jvm.options && /usr/local/bin/docker-entrypoint.sh elasticsearch -Ediscovery.type=single-node
variables:
DB_TYPE: sqlite
RAILS_ENV: development
ELASTICSEARCH_URL: "http://elasticsearch:9200"
before_script:
- bundle config --local path /projectforum/vendor
script:
# Check elasticsearch is running
- sleep 10
- 'for i in $(seq 1 10); do curl "$ELASTICSEARCH_URL/_cat/health" && s=0 && break || s=$? && sleep 5; done; (exit $s)'
- sleep 5
- 'for i in $(seq 1 12); do curl "$ELASTICSEARCH_URL/_cat/health" && s=0 && break || s=$? && sleep 5; done; (exit $s)'
# Check seeding
- bin/rails db:environment:set
- DB_TYPE=sqlite bundle exec rake db:schema:load
- DB_TYPE=sqlite bundle exec rake db:seed
- DB_TYPE=sqlite bundle exec rails db:schema:load
- DB_TYPE=sqlite bundle exec rails db:seed
# -------------------------------------------------------------------------------------------------
# Reports
......@@ -234,12 +228,11 @@ coverage-report:
- System Test
coverage: '/LOC\s\(\d+\.\d+%\)\scovered/'
before_script:
- bundle config --local path /projectforum/vendor
- mkdir /pf_tests
- cp -r . /pf_tests
script:
- cd /pf_tests
- bundle exec rake coverage:combine
- bundle exec rails coverage:combine
after_script:
- cd /pf_tests
- cp coverage -r --parents $CI_PROJECT_DIR
......@@ -253,10 +246,8 @@ coverage-report:
# needs:
# - Test
# - System Test
# before_script:
# - bundle config --local path /projectforum/vendor
# script:
# - bundle exec rake test:combine
# - bundle exec rails test:combine
# artifacts:
# paths:
# - test/reports/junit.xml
......@@ -280,7 +271,6 @@ deploy-staging:
- mkdir -p ~/.ssh
- ssh-keyscan eiptest.ewi.tudelft.nl >> ~/.ssh/known_hosts
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
- bundle config --local path /projectforum/vendor
script:
- BRANCH=development bundle exec cap staging deploy
environment:
......@@ -304,7 +294,6 @@ deploy-production:
- mkdir -p ~/.ssh
- ssh-keyscan projectforum.tudelft.nl >> ~/.ssh/known_hosts
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
- bundle config --local path /projectforum/vendor
script:
- BRANCH=master bundle exec cap production deploy
environment:
......
This diff is collapsed.
......@@ -129,3 +129,49 @@ Installing rglpk can fail if you have an incompatible version of the libglpk lib
3. `make all`
4. `sudo make install`
5. `sudo ldconfig`
## License
Project Forum
Copyright (C) 2022 EIP
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
This copyright and license applies to all of the source code files in the following folders, unless if indicated otherwise in a source code file:
* app/assets/images/tutorials
* app/channels
* app/controllers
* app/helpers
* app/jobs
* app/mailers
* app/models
* app/service
* app/validators
* app/views
* db
* lib
* manuals
* public (excluding images)
* test
The following folders carry both source files from the project (covered under the AGPLv3 license) as well as source files from libraries, to which a different license may apply. Please review the notice in each file carefully. If no notice is present, the source code file falls under the copyright and license of Project Forum (see above):
* app/assets/javascripts
* app/assets/stylesheets
* app/assets/config
* config
The following folders contain source code files from libraries, which is not covered by the license for Project Forum:
* bin
* vendor
If any doubt exists about the licensing of a particular source file, or if you otherwise would like more information on setting up or using Project Forum for your own institution, please contact EIP with the contact information listed on our website, https://eip.ewi.tudelft.nl.
......@@ -50,12 +50,14 @@ module Admin
def filter_params
params.permit(
id: [], student_type: []
id: [], student_type: [], experiment_projects: { round: [], course_edition: [] }
)
end
def before_filtering(users)
users.where(id: CourseParticipation.participants_research_practicum.ids)
.left_outer_joins(:experiment_projects)
.distinct
end
end
end
......@@ -66,7 +66,7 @@ module Admin
def filter_params
params.permit(
id: [], user_id: [], experiment_project_id: [], experiment_projects: { round: [], department: [] }
id: [], user_id: [], experiment_project_id: [], experiment_projects: { round: [], department: [], course_edition: [] }
)
end
......
......@@ -24,6 +24,8 @@ module ParamsConcerns
# that are accepted via a controller for a company.
def admin_project_params_keys
BASE + FULL + [
shared_offerer_projects_attributes: \
ParamsConcerns::SharedOffererProjects::BASE + %i[id _destroy],
groups_attributes: \
ParamsConcerns::Groups::BASE + %i[id _destroy] + [
memberships_attributes: \
......
module ParamsConcerns
module SharedOffererProjects
extend ActiveSupport::Concern
BASE = %i[offerer_id].freeze
end
end
......@@ -150,6 +150,8 @@ class GenericProjectsController < ProjectsController
copy_reoffer_images
copy_reoffer_shared_offerers
build_reoffer_recurrent_project_structure
# assign course-specific roles
......@@ -223,6 +225,16 @@ class GenericProjectsController < ProjectsController
redirect_to generic_project_path(@specific_project)
end
def add_shared_offerer
add_project_shared_offerer
redirect_to generic_project_path(@specific_project)
end
def remove_shared_offerer
remove_project_shared_offerer
redirect_to generic_project_path(@specific_project)
end
def resume_recurrence
authorize! :update, @project
success = resume_project_recurrence
......
......@@ -37,7 +37,7 @@ module Projects
def destroy
# If there is no id and email is present, look up by email
if params[:id] == '-1' && params[:email].present?
student = User.find_by(email: params[:email].lower)
student = User.find_by(email: params[:email].downcase)
# If the student does not exist, we expect an error, which is what we want.
@student_preference = StudentPreference.find_by(user_id: current_user.id, student_id: student.id, course_edition_id: @course_edition.id)
else
......
......@@ -92,6 +92,34 @@ class ProjectsController < ApplicationController
end
end
# Adds a shared offerer to the current project
def add_project_shared_offerer
offerer = retrieve_shared_offerer_to_add
return unless offerer
if @project.shared_offerer_projects.find_by(offerer_id: offerer.id) || offerer.id == @project.offerer_id
flash[:danger] = 'This user/organisation already has access to this proposal.'
return
end
@project.shared_offerer_projects.create!(offerer: offerer)
flash[:success] = 'Successfully granted access'
end
# Adds a shared offerer to the current project
def remove_project_shared_offerer
shared_offerer_project_id = params[:shared_offerer_project_id]
raise ActionController::BadRequest unless shared_offerer_project_id
shared_offerer_project = @project.shared_offerer_projects.find_by(id: shared_offerer_project_id)
raise ActionController::BadRequest unless shared_offerer_project
shared_offerer_project.destroy!
flash[:success] = 'Successfully revoked access'
end
# -----------------------------------------------------------------------------------------------
# Destruction helpers
......@@ -235,6 +263,15 @@ class ProjectsController < ApplicationController
end
end
# Copies shared offerers to the current project from the project being reoffered
def copy_reoffer_shared_offerers
return unless @project_to_reoffer
@project_to_reoffer.shared_offerer_projects.each do |shared|
@project.shared_offerer_projects.create(offerer_id: shared.offerer_id)
end
end
# -----------------------------------------------------------------------------------------------
# Params helpers
......@@ -433,4 +470,35 @@ class ProjectsController < ApplicationController
end
end
end
def retrieve_shared_offerer_to_add
offerer_id = params[:offerer_id]
if offerer_id
offerer = Offerer.find_by(id: offerer_id)
return offerer if offerer && !offerer.user?
end
user_email = params[:user_email]&.strip&.downcase
raise ActionController::BadRequest unless user_email
user = User.find_by(email: user_email)
return user.acting_as if user
if ldap_supported?(user_email)
user = generate_user_from_ldap(user_email)
return user.acting_as if user
# The user was not found in LDAP, so this email is invalid
flash[:danger] = "A user with email '#{user_email}' does not exist at TU Delft. " \
'Is this email address correct?'
else
flash[:danger] = "User with email '#{user_email}' not found. Please make sure that the email is " \
'correct and the user has registered on ProjectForum.'
end
nil
end
end
......@@ -187,7 +187,7 @@ module CrudHelper
if action.to_s == 'destroy' || action.to_s == 'delete'
obj.destroyed?
else
obj.valid? && obj.persisted?
(obj.errors.none? && obj.valid?) && obj.persisted?
end
end
......
......@@ -72,7 +72,11 @@ module LinksHelper
name = object.send(name) if name.is_a?(Symbol) && object.respond_to?(name)
if can? :show, object
link_to(name, url, html_options, &block)
if block_given?
link_to(url, html_options, &block)
else
link_to(name, url, html_options)
end
else
content_tag(:span, name, html_options, &block)
end
......
......@@ -25,4 +25,15 @@ module NameHelper
def groups_name(count = 2)
group_name.pluralize(count)
end
def company_name
name = current_user&.company_name
return name if name
'company'
end
def companies_name(count = 2)
company_name.pluralize(count)
end
end
......@@ -140,8 +140,8 @@ class Ability
def unconfirmed_employee_rights(user)
# Project
can %i[index create], Project
can %i[show read_detailed reoffer], Project, id: Project.with_role(:client, user).ids
can %i[update update_role_client], Project, id:
can %i[show read_detailed reoffer add_shared_offerer remove_shared_offerer], Project, id: Project.with_role(:client, user).ids
can %i[update update_role_client attempt_to_destroy], Project, id:
Project.with_role(:client, user)
.joins(:course_edition)
.where(course_editions: { id: CourseEdition.active.not_hidden }).ids
......@@ -221,8 +221,10 @@ class Ability
# Project
can %i[index create], Project
can %i[show read_detailed reoffer], Project, id: user.offerer_projects.ids
can %i[add_shared_offerer remove_shared_offerer], Project, id: user.own_offerer_projects.ids
can %i[update update_role_client], Project, id: user.updatable_offerer_projects.ids
can :destroy, Project, id: user.offerer_projects.destroyable.map(&:id)
can :attempt_to_destroy, Project, id: user.updatable_own_offerer_projects.ids
can :destroy, Project, id: user.own_offerer_projects.destroyable.map(&:id)
# Group
can :read, Group, id: user.offerer_groups.ids
......@@ -290,6 +292,14 @@ class Ability
can %i[read access join_interest_list], CourseEdition, id: CourseEdition.open_for_offering_projects.internal.ids
can %i[read access join_interest_list], CourseEdition, id: user.course_editions_where_has_project.not_hidden.ids
# Abilities for staff members enrolled in courses
can %i[read access set_preferences join_interest_list], CourseEdition, id: user.approved_course_editions.not_hidden.ids
can :read, CourseEdition, id: user.pending_course_editions.active.ids
can %i[access set_preferences], CourseEdition, id: user.pending_course_editions.allowing_pending_students_to_see_projects.active.ids
course_edition_content_rights(user.pending_course_editions.allowing_pending_students_to_see_projects.ids)
course_edition_content_rights(user.approved_course_editions.ids)
course_edition_interest_rights(user.pending_or_approved_course_editions.ids)
# Course Edition Availability
can %i[index create], Availability
......
......@@ -7,7 +7,5 @@ module ElasticSearchable
# Use the Rails env in the index name to prevent issues of test indices overriding development/production indices
index_name "#{Rails.env}_#{model_name.collection.gsub(%r{/}, '-')}"
after_touch { __elasticsearch__.index_document }
end
end
......@@ -312,9 +312,19 @@ module UserConcerns
end
end
def shared_offerer_projects
Project.joins(:shared_offerer_projects).where(shared_offerer_projects: { offerer_id: involved_offerers })
end
def own_offerer_projects
Project.where(id: cliented_projects)
.or(Project.where(offerer_id: involved_offerers))
end
def offerer_projects(offerer_type = nil)
Project.where(id: cliented_projects).or(Project.where(offerer_id: involved_offerers))
.where(offerer_id: Offerer.with_type(offerer_type))
own_offerer_projects
.or(Project.where(id: shared_offerer_projects))
.where(offerer_id: Offerer.with_type(offerer_type))
end
def active_offerer_projects(offerer_type = nil)
......@@ -348,6 +358,11 @@ module UserConcerns
.where(course_editions: { id: CourseEdition.active.not_hidden })
end
def updatable_own_offerer_projects
own_offerer_projects.joins(:course_edition)
.where(course_editions: { id: CourseEdition.active.not_hidden })
end
def updatable_offerer_groups
Group.locked.where(project_id: offerer_projects).or(Group.approved.where(project_id: offerer_projects))
end
......@@ -407,11 +422,11 @@ module UserConcerns
'project'
end
else
names = programmes.map { |p| p.configuration.present? ? p.configuration.group_text(false, false) : 'student' }.uniq
names = programmes.map { |p| p.configuration.present? ? p.configuration.group_text(false, false) : 'group' }.uniq
if names.size == 1
names[0]
else
'student'
'group'
end
end
end
......
......@@ -283,21 +283,19 @@ class CourseEdition < ApplicationRecord
User.staff.or(User.admin).on_interest_list_in_course_edition(self)
end
after_create do
duplicate_projects
end
after_create :duplicate_projects, if: (-> { !(recurrent_edition_id.nil? || active_period_id.nil?) })
# For course editions which descend from a recurring edition (which also have an active period set)
# this method will copy over the approved projects from the recurrent edition which match the active period.
# This method is triggered after the creation of a course edition.
def duplicate_projects
unless recurrent_edition_id.nil? || active_period_id.nil?
recurrent_edition.projects.each do |project|
if project.status == 'approved' && project.periods.select { |period| period.id == active_period.id }.any?
project.copy_project(self)
end
recurrent_edition.projects.each do |project|
if project.status == 'approved' && project.periods.select { |period| period.id == active_period.id }.any?
project.copy_recurrent_project(self, self, :base)
end
end
raise ActiveRecord::Rollback if errors.include?(:base)
end
# ===== Other =====
......
......@@ -3,6 +3,9 @@ class Offerer < ApplicationRecord
has_many :projects, dependent: :nullify
has_many :shared_offerer_projects, dependent: :destroy
has_many :shared_projects, through: :shared_offerer_projects, source: :project, class_name: 'Project'
scope :users, -> { where(actable_type: User.model_name.name) }
scope :companies, -> { where(actable_type: Company.model_name.name) }
......
......@@ -17,6 +17,9 @@ class Project < ApplicationRecord
belongs_to :offerer, counter_cache: true
belongs_to :course_edition
has_many :programmes, through: :course_edition
has_many :shared_offerer_projects, dependent: :destroy
accepts_nested_attributes_for :shared_offerer_projects, allow_destroy: true
has_many :shared_offerers, through: :shared_offerer_projects, source: :offerer, class_name: 'Offerer'
has_many :images, as: :imageable, dependent: :destroy
accepts_nested_attributes_for :images, allow_destroy: true
......@@ -107,12 +110,19 @@ class Project < ApplicationRecord
CourseEdition.descendants_of(course_edition).before_started.find_each do |course_edition|
if periods.select { |period| period.id == course_edition.active_period.id }.any? &&
course_edition.projects.where(original_project_id: id).none?