diff --git a/Gemfile b/Gemfile index 7652b503..71acfe10 100644 --- a/Gemfile +++ b/Gemfile @@ -22,7 +22,6 @@ gem "pundit", "~> 1.1.0" gem "recaptcha", require: "recaptcha/rails" gem "loofah", "<= 2.20.0" gem "whenever", require: false # for cron jobs -gem "squeel" # , '~> 1.1.1' # until version 1.1.2 released gem "tilt" gem "simple-navigation", "3.11.0" gem "simple_form", "3.2.1" diff --git a/Gemfile.lock b/Gemfile.lock index a013d206..0d739b05 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -223,8 +223,6 @@ GEM pdfkit (0.8.2) pg (0.21.0) pkg-config (1.5.1) - polyamorous (1.1.0) - activerecord (>= 3.0) prawn (2.2.2) pdf-core (~> 0.7.0) ttfunk (~> 1.5) @@ -363,10 +361,6 @@ GEM actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) - squeel (1.2.3) - activerecord (>= 3.0) - activesupport (>= 3.0) - polyamorous (~> 1.1.0) ssrf_filter (1.0.8) standard (1.0.5) rubocop (= 1.12.1) @@ -468,7 +462,6 @@ DEPENDENCIES simplecov sinatra spring - squeel standard strong_presenter (~> 0.2.2) superfish-rails (~> 1.6.0) diff --git a/app/controllers/contests_controller.rb b/app/controllers/contests_controller.rb index 864b0bd0..172e35cf 100644 --- a/app/controllers/contests_controller.rb +++ b/app/controllers/contests_controller.rb @@ -24,13 +24,24 @@ def browse case params[:filter].to_s when "active" raise Pundit::NotAuthorizedError unless user_signed_in? - @contests = Contest.joins(:contest_relations).where { (contest_relations.user_id == my { current_user.id }) & (contest_relations.finish_at > Time.now) }.order("end_time ASC") + @contests = Contest + .joins(:contest_relations) + .where(contest_relations: { user_id: current_user.id }) + .where("contest_relations.finish_at > ?", Time.now) + .order("end_time ASC") when "current" - @contests = visible_contests.where { (start_time < Time.now + 30.minutes) & (end_time > Time.now) }.order("end_time ASC") + @contests = visible_contests + .where("start_time < ?", Time.now + 30.minutes) + .where("end_time > ?", Time.now) + .order("end_time ASC") when "upcoming" - @contests = visible_contests.where { (start_time > Time.now + 30.minutes) }.order("start_time ASC") + @contests = visible_contests + .where("start_time > ?", Time.now + 30.minutes) + .order("start_time ASC") when "past" - @contests = visible_contests.where { (end_time < Time.now) }.order("end_time DESC") + @contests = visible_contests + .where("end_time < ?", Time.now) + .order("end_time DESC") else raise Pundit::NotAuthorizedError end diff --git a/app/controllers/groups/members_controller.rb b/app/controllers/groups/members_controller.rb index 7e42bba6..a421ddeb 100644 --- a/app/controllers/groups/members_controller.rb +++ b/app/controllers/groups/members_controller.rb @@ -97,8 +97,8 @@ def invites redirect_to(invites_members_group_path(@group), notice: "#{@user.username} has been invited to join this group") end else # get request - @pending_requests = @group.invitations.pending.order(:created_at).reverse_order - @requests = @group.invitations.closed.order(:created_at).reverse_order + @pending_requests = @group.invitations.merge(Request.pending).order(created_at: :desc) + @requests = @group.invitations.merge(Request.closed).order(created_at: :desc) end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index b7942003..763c32b7 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -2,6 +2,6 @@ class HomeController < ApplicationController def home @mygroups = current_user.try(:groups) @problem_set_associations = Group.find(0).problem_set_associations - @contests = policy_scope(Contest).where { (end_time > Time.now) }.order("end_time ASC") + @contests = policy_scope(Contest).not_ended.order("end_time ASC") end end diff --git a/app/controllers/problems_controller.rb b/app/controllers/problems_controller.rb index 3db6593f..a830a994 100644 --- a/app/controllers/problems_controller.rb +++ b/app/controllers/problems_controller.rb @@ -91,7 +91,13 @@ def submissions if current_user.openbook? @submissions = @problem.submission_history(current_user) else - start_time = current_user.contest_relations.joins(contest: {problem_set: :problems}).where { (started_at <= DateTime.now) & (finish_at > DateTime.now) & (contest.problem_set.problems.id == my { params[:id] }) }.minimum(:started_at) + start_time = current_user + .contest_relations + .joins(contest: {problem_set: :problems}) + .where("started_at <= :now AND finish_at > :now", now: DateTime.now) + .where(problems: {id: params[:id]}) + .minimum(:started_at) + @submissions = @problem.submission_history(current_user, start_time) end diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index a5d58d44..e6dd4588 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -9,7 +9,12 @@ def permitted_params def show @user = User.find(params[:id]) authorize @user, :show? - @solved_problems = @user.user_problem_relations.where(ranked_score: 100).joins(:problem).select([:problem_id, :ranked_submission_id, {problem: :name}]).order("problems.name") + @solved_problems = @user + .user_problem_relations + .where(ranked_score: 100) + .joins(:problem) + .select("problems.name", "problem_id", "ranked_submission_id") + .order("problems.name") end def edit diff --git a/app/models/contest.rb b/app/models/contest.rb index fce12562..61fba1be 100644 --- a/app/models/contest.rb +++ b/app/models/contest.rb @@ -24,6 +24,9 @@ class Contest < ActiveRecord::Base # public = everyone, protected = in group, private = competitors OBSERVATION = Enumeration.new 0 => :public, 1 => :protected, 2 => :private + scope :not_ended, -> { where("end_time > ?", Time.current) } + scope :publicly_observable, -> { where(observation: OBSERVATION[:public]) } + before_save do # update the end time that was cached if duration_changed? || end_time_changed? contest_relations.find_each do |relation| diff --git a/app/models/contest_relation.rb b/app/models/contest_relation.rb index 842ce85a..5ad74803 100644 --- a/app/models/contest_relation.rb +++ b/app/models/contest_relation.rb @@ -7,7 +7,7 @@ class ContestRelation < ActiveRecord::Base belongs_to :school belongs_to :supervisor, class_name: :User - scope :active, -> { where { (started_at <= DateTime.now) & (finish_at > DateTime.now) } } + scope :active, -> { where("started_at <= :now AND finish_at > :now", now: DateTime.now) } scope :absent, -> { where(checked_in: false) } scope :user, ->(u_id) { where(user_id: u_id) } diff --git a/app/models/contest_supervisor.rb b/app/models/contest_supervisor.rb index 771e02df..97849c35 100644 --- a/app/models/contest_supervisor.rb +++ b/app/models/contest_supervisor.rb @@ -36,7 +36,10 @@ def contest_relations def potential_contestants if site_type == "School" - User.where { |user| (user.school_id == site_id) & ((user.school_graduation >= contest.end_time) | ((user.school_graduation.nil?) & (user.created_at >= DateTime.now.advance(years: -1)))) & (user.id << contest.registrants) } + User + .where(school_id: site_id) + .where("(users.school_graduation >= :contest_end_time) OR ((users.school_graduation IS NULL) AND (users.created_at >= :year_ago))", contest_end_time: contest.end_time, year_ago: DateTime.now.advance(years: -1)) + .where.not(id: contest.registrants) else [] # not implemented end diff --git a/app/models/problem_set.rb b/app/models/problem_set.rb index 1baea68f..20f20b75 100644 --- a/app/models/problem_set.rb +++ b/app/models/problem_set.rb @@ -17,11 +17,19 @@ class ProblemSet < ActiveRecord::Base validates :name, presence: true def problems_with_scores_by_user(user_id) - problems.joins("LEFT OUTER JOIN user_problem_relations ON user_problem_relations.problem_id = problems.id AND user_problem_relations.user_id = #{user_id} LEFT OUTER JOIN submissions ON submissions.id = user_problem_relations.submission_id").select([:id, :name, :test_error_count, :test_warning_count, :test_status, {submissions: [:points, :maximum_points], problem_set_problems: :weighting}]) + problems + .joins("LEFT OUTER JOIN user_problem_relations ON user_problem_relations.problem_id = problems.id AND user_problem_relations.user_id = #{user_id} LEFT OUTER JOIN submissions ON submissions.id = user_problem_relations.submission_id") + .select( + "id", "name", "test_error_count", "test_warning_count", "test_status", "submissions.points", "submissions.maximum_points", "problem_set_problems.weighting" + ) end - def for_contestant? u_id - contests.joins(:contest_relations).where(contest_relations: {user_id: u_id}).where { {contest_relations => sift(:active)} }.any? + def for_contestant?(u_id) + contests + .joins(:contest_relations) + .where(contest_relations: {user_id: u_id}) + .merge(ContestRelation.active) + .any? end def for_owner? u_id diff --git a/app/models/request.rb b/app/models/request.rb index 2126c1b2..63fde6e8 100644 --- a/app/models/request.rb +++ b/app/models/request.rb @@ -16,9 +16,23 @@ class Request < ActiveRecord::Base status == key end end - scope :pending, -> { where { (status == STATUS[:pending]) & (expired_at > DateTime.now) } } - scope :expired, -> { where { (status == STATUS[:pending]) & (expired_at <= DateTime.now) } } - scope :closed, -> { where { (status != STATUS[:pending]) | (expired_at <= DateTime.now) } } + + # I would prefer to just use expired and not_expired, but these + # names are in used below for a slightly different use. + scope :past_expiry, -> { where("expired_at <= ?", DateTime.now) } + scope :not_past_expiry, -> { where("expired_at > ?", DateTime.now) } + + # IMO a better name for this would be "pending", but see below + scope :status_pending, -> { where(status: STATUS[:pending]) } + + # These scopes have weird names. + # * Pending is redefined a scope defined above. + # * Expired you wouldn't usually expect to check status + scope :pending, -> { status_pending.merge(not_past_expiry) } + scope :expired, -> { status_pending.merge(past_expiry) } + + # TODO(Rails5): Convert to where.not(pending).or(past_expiry) + scope :closed, -> { where("status != ? OR expired_at <= ?", STATUS[:pending], DateTime.now) } def pending? status == STATUS[:pending] && (expired_at == Float::INFINITY || expired_at > DateTime.now) diff --git a/app/policies/contest_policy.rb b/app/policies/contest_policy.rb index 1c0b5f22..5c0f85bc 100644 --- a/app/policies/contest_policy.rb +++ b/app/policies/contest_policy.rb @@ -5,25 +5,30 @@ def resolve return scope.all end - # following uses advanced squeel - scope.where do |contests| - public_observation = contests.observation == Contest::OBSERVATION[:public] - grouped = contests.id.in(GroupContest.where do |gc| - group_contests = (gc.group_id == 0) - group_contests |= (gc.group_id >> user.groups.select(:id)) if user - group_contests - end.select(:contest_id)) - visible_contests = public_observation | grouped - - if user - registered = contests.id.in(user.contest_relations.select(:contest_id)) - owned = contests.owner_id == user.id - supervising = contests.id.in(user.contest_supervising.select(:contest_id)) - visible_contests |= registered | owned | supervising - end - - visible_contests - end + sql = <<~SQL + contests.observation = :public + OR contests.id IN ( + SELECT + contest_id + FROM + group_contests + WHERE + group_id = 0 + OR group_id IN (:user_groups) + ) + OR contests.id IN (:allowed_contest_ids) + OR contests.owner_id = :user_id + SQL + + allowed_contest_ids = [] + allowed_contest_ids = user.contest_relations.pluck(:contest_id) + user.contest_supervising.pluck(:contest_id) if user + + scope.where(sql, { + public: Contest::OBSERVATION[:public], + user_groups: user&.groups&.pluck(:id), + allowed_contest_ids: allowed_contest_ids, + user_id: user&.id, + }) end end @@ -34,12 +39,22 @@ def registered? def current_contestant? return false unless user # signed in - record.contest_relations.where { |relation| (relation.user_id == user.id) & (relation.started_at <= DateTime.now) & (relation.finish_at > DateTime.now) }.exists? + + record + .contest_relations + .where(user_id: user.id) + .where("started_at <= :now AND finish_at > :now", DateTime.now) + .exists? end def current_or_past_contestant? return false unless user # signed in - record.contest_relations.where { |relation| (relation.user_id == user.id) & (relation.started_at <= DateTime.now) }.exists? + + record + .contest_relations + .where(user_id: user.id) + .where("started_at <= ?", DateTime.now) + .exists? end def index? @@ -87,7 +102,7 @@ def unfinalize? def startable? return false unless user # signed in - user.is_staff? or registered? or record.groups.where(id: 0).exists? or record.groups.joins(:memberships).where(memberships: {member_id: user.id}).exists? + user.is_staff? || registered? || record.groups.where(id: 0).exists? || record.groups.joins(:memberships).where(group_memberships: { member_id: user.id }).exists? end def start? diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 234d161d..d34a1aa2 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -4,7 +4,11 @@ def resolve(source = nil) if user.is_staff? scope.all else - scope.where { |groups| (groups.id == 0) | (groups.visibility == Group::VISIBILITY[:public]) | (groups.owner_id == user.id) } + scope.where( + "id = 0 OR visibility = :public OR owner_id = :user_id", + public: Group::VISIBILITY[:public], + user_id: user.id + ) end end end diff --git a/app/policies/problem_policy.rb b/app/policies/problem_policy.rb index 08267d77..c51ca2bf 100644 --- a/app/policies/problem_policy.rb +++ b/app/policies/problem_policy.rb @@ -32,12 +32,21 @@ def show? return true if user && user.is_staff? if user && user.competing? - return record.contest_relations.where { |relation| (relation.user_id == user.id) & (relation.started_at <= DateTime.now) & (relation.finish_at > DateTime.now) }.exists? + return record + .contest_relations + .where(contest_relations: { user_id: user.id }) + .where("started_at <= :now AND finish_at > :now", now: DateTime.now) + .exists? end return true if record.groups.where(id: 0).exists? return false unless user # signed in - user.owns(record) or record.group_memberships.where { |membership| (membership.member_id == user.id) }.exists? + return true if user.owns(record) + + record + .group_memberships + .where(group_memberships: { member_id: user.id }) + .exists? end def access? diff --git a/app/policies/problem_set_policy.rb b/app/policies/problem_set_policy.rb index ab39a2c8..5a9d4893 100644 --- a/app/policies/problem_set_policy.rb +++ b/app/policies/problem_set_policy.rb @@ -24,16 +24,20 @@ def manage? def show? return true if user && user.is_staff? if user && user.competing? - return user.contest_relations.where { |relation| (relation.started_at <= DateTime.now) & (relation.finish_at > DateTime.now) & (relation.contest_id >> record.contest_ids) }.exists? + return user + .contest_relations + .where("started_at <= :now AND finish_at > :now", now: DateTime.now) + .where("contest_relations.contest_id IN (:contest_ids)", contest_ids: record.contest_ids) + .exists? end return true if record.groups.where(id: 0).exists? return false unless user # signed in - user.owns(record) or record.group_memberships.where { |membership| (membership.member_id == user.id) }.exists? + user.owns(record) || record.group_memberships.where(member_id: user.id).exists? end def create? return false unless user # signed in - super or user.is_staff? or user.is_any?([:organiser, :author]) + super || user.is_staff? || user.is_any?([:organiser, :author]) end end diff --git a/app/policies/request_policy.rb b/app/policies/request_policy.rb index 641e86cb..8c073fe9 100644 --- a/app/policies/request_policy.rb +++ b/app/policies/request_policy.rb @@ -4,7 +4,7 @@ def resolve if user.is_admin? scope.all else - scope.where { (requester_id == user.id) | (requestee_id == user.id) } + scope.where("requester_id = :user_id OR requestee_id = :user_id", user_id: user.id) end end end diff --git a/app/policies/submission_policy.rb b/app/policies/submission_policy.rb index ce12dd79..e7d51e74 100644 --- a/app/policies/submission_policy.rb +++ b/app/policies/submission_policy.rb @@ -4,20 +4,31 @@ def resolve if user.is_staff? scope.all elsif user.competing? - problem_set_ids = ContestRelation.where { |contest_relations| (contest_relations.user_id == user.id) & (contest_relations.started_at <= DateTime.now) & (contest_relations.finish_at > DateTime.now) }.joins(:contest).select(contest: :problem_set_id) - scope.joins(problem: :problem_sets).where(problem: {problem_sets: {id: problem_set_ids}}, user_id: user.id) + problem_set_ids = ContestRelation + .where(user_id: user.id) + .where("contest_relations.started_at <= ?", DateTime.now) + .where("contest_relations.finish_at > ?", DateTime.now) + .joins(:contest) + .select("contests.problem_set_id") + + scope + .joins(problem: :problem_sets) + .where(user_id: user.id) + .where(problem_sets: {id: problem_set_ids}) else - scope.joins(:problem).where { |submission| (submission.user_id == user.id) | (submission.problem.owner_id == user.id) } + scope + .joins(:problem) + .where("submissions.user_id = :user_id OR problems.owner_id = :user_id", user_id: user.id) end end end def inspect? - super or (record.is_a?(Submission) && policy(record.problem).try(:inspect?) && !user.competing?) + super || (record.is_a?(Submission) && policy(record.problem).try(:inspect?) && !user.competing?) end def update? - super or (record.is_a?(Submission) && policy(record.problem).try(:update?) && !user.competing?) + super || (record.is_a?(Submission) && policy(record.problem).try(:update?) && !user.competing?) end def index? @@ -25,7 +36,7 @@ def index? end def show? - inspect? or scope.where(id: record.id).exists? + inspect? || scope.where(id: record.id).exists? end def rejudge? diff --git a/app/workers/judge_submission_worker.rb b/app/workers/judge_submission_worker.rb index 658881c2..a759846f 100644 --- a/app/workers/judge_submission_worker.rb +++ b/app/workers/judge_submission_worker.rb @@ -13,10 +13,16 @@ def self.judge(submission, queue: nil, delay: 0) user = submission.user if user.competing? && Pundit.policy(user, submission).show? if rejudging # re-judging, later submissions are more important - later = Submission.where(problem_id: submission.problem_id, user_id: submission.user_id).where { created_at > submission.created_at }.count + later = Submission + .where(problem_id: submission.problem_id, user_id: submission.user_id) + .where("created_at > ?", submission.created_at) + .count priority = 18 - [later * 2, 10].min else # first time judging, earlier submissions get better priority (so other people get a chance to have their stuff judged) - prev = Submission.where(problem_id: submission.problem_id, user_id: submission.user_id).where { created_at < submission.created_at }.count + prev = Submission + .where(problem_id: submission.problem_id, user_id: submission.user_id) + .where("created_at < ?", submission.created_at) + .count priority = 20 - [prev, 10].min end @@ -102,7 +108,7 @@ def judge resource_limits = {mem: memory_limit * 1024, time: time_limit, extra_time: extra_time, wall_time: wall_time, stack: stack_limit, processes: submission.language.processes} # prerequisites - prereqs = problem.test_cases.where(id: problem.prerequisite_sets.joins(:test_case_relations).select(test_case_relations: :test_case_id)) + prereqs = problem.test_cases.where(id: problem.prerequisite_sets.joins(:test_case_relations).select("test_case_relations.test_case_id")) prereqs.each do |test_case| result["test_cases"][test_case.id] = judge_test_case(test_case, run_command, eval_command, resource_limits) unless result["test_cases"].has_key?(test_case.id) diff --git a/db/migrate/20131221014009_create_user_problem_relation.rb b/db/migrate/20131221014009_create_user_problem_relation.rb index 8098d012..2a426baf 100644 --- a/db/migrate/20131221014009_create_user_problem_relation.rb +++ b/db/migrate/20131221014009_create_user_problem_relation.rb @@ -2,7 +2,7 @@ class CreateUserProblemRelation < ActiveRecord::Migration def change reversible do |dir| dir.up do - missing_user_ids = Submission.where { |submission| submission.user_id << User.select(:id) }.pluck(:user_id) + missing_user_ids = Submission.where("submissions.user_id NOT IN (SELECT id FROM users)").pluck(:user_id) Submission.where(user_id: missing_user_ids).delete_all ContestRelation.where(user_id: missing_user_ids).destroy_all diff --git a/spec/factories/contest_supervisors.rb b/spec/factories/contest_supervisors.rb index 5eb068db..5f6cf9df 100644 --- a/spec/factories/contest_supervisors.rb +++ b/spec/factories/contest_supervisors.rb @@ -1,10 +1,9 @@ # Read about factories at https://github.com/thoughtbot/factory_bot FactoryBot.define do - factory :contest_supervisor, class: "ContestSupervisors" do - contest_id { 1 } - user_id { 1 } - site_type { "MyString" } - site_id { 1 } + factory :contest_supervisor, class: "ContestSupervisor" do + association :contest + association :user + association :site, factory: :school end end diff --git a/spec/models/contest_supervisor_spec.rb b/spec/models/contest_supervisor_spec.rb index 2a510328..512d4870 100644 --- a/spec/models/contest_supervisor_spec.rb +++ b/spec/models/contest_supervisor_spec.rb @@ -1,5 +1,45 @@ require "spec_helper" describe ContestSupervisor do - pending "add some examples to (or delete) #{__FILE__}" + subject(:contest_supervisor) { FactoryBot.create(:contest_supervisor, contest: contest) } + + let(:contest) { FactoryBot.create(:contest) } + + describe "#potential_contestants" do + let!(:students) do + 10.times.map { + FactoryBot.create(:user, country_code: "NZ", school: contest_supervisor.site) + } + end + + context "when no-one is registered for the contest" do + it "includes all the students at the school" do + expect(contest_supervisor.potential_contestants).to match_array(students) + end + end + + context "when some people are registered for the contest" do + before do + students.take(5).each do |student| + contest.registrants << student + end + end + + it "includes the remaining students at the school" do + expect(contest_supervisor.potential_contestants).to match_array(students.drop(5)) + end + end + + context "when no-one is registered for the contest" do + before do + students.each do |student| + contest.registrants << student + end + end + + it "return an empty list" do + expect(contest_supervisor.potential_contestants).to be_empty + end + end + end end diff --git a/spec/requests/submissions_request_spec.rb b/spec/requests/submissions_request_spec.rb new file mode 100644 index 00000000..c4f5388c --- /dev/null +++ b/spec/requests/submissions_request_spec.rb @@ -0,0 +1,44 @@ +require "spec_helper" + +RSpec.describe SubmissionsController, type: :request do + let(:problem) { FactoryBot.create(:problem) } + let(:problem_set) { FactoryBot.create(:problem_set, problems: [problem]) } + let(:user) { FactoryBot.create(:user, groups: [group]) } + let(:group) { FactoryBot.create(:group) } + let!(:submission) { FactoryBot.create(:submission, problem: problem, user: user) } + + before do + problem_set.groups << group + + sign_in(user) + end + + describe "GET /problems/:id/submissions" do + it "doesn't explode" do + get submissions_problem_path(problem) + + expect(response).to have_http_status(:success) + end + end + + describe "GET /submissions/my" do + context "when not in an active contest" do + it "doesn't explode" do + get my_submissions_path + + expect(response).to have_http_status(:success) + end + end + + context "when in an active contest" do + let(:contest) { FactoryBot.create(:contest, problem_set: problem_set) } + let!(:contest_relation) { FactoryBot.create(:contest_relation, user: user, contest: contest, finish_at: 99.years.from_now) } + + it "doesn't explode" do + get my_submissions_path + + expect(response).to have_http_status(:success) + end + end + end +end