<commit_before>#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas 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, version 3 of the License.
#
# Canvas 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 <http://www.gnu.org/licenses/>.
#

class Submission < ActiveRecord::Base
  include SendToStream
  attr_protected :submitted_at
  attr_readonly :assignment_id
  belongs_to :attachment # this refers to the screenshot of the submission if it is a url submission
  belongs_to :assignment
  belongs_to :user
  belongs_to :grader, :class_name => 'User'
  belongs_to :group
  belongs_to :media_object
  belongs_to :student, :class_name => 'User', :foreign_key => :user_id
  has_many :submission_comments, :order => 'created_at', :dependent => :destroy
  has_many :assessment_requests, :as => :asset
  has_many :assigned_assessments, :class_name => 'AssessmentRequest', :as => :assessor_asset
  belongs_to :quiz_submission
  has_one :rubric_assessment, :as => :artifact, :conditions => {:assessment_type => "grading"}
  has_many :rubric_assessments, :as => :artifact
  has_many :attachment_associations, :as => :context
  has_many :attachments, :through => :attachment_associations
  serialize :turnitin_data, Hash
  validates_presence_of :assignment_id, :user_id
  validates_length_of :body, :maximum => maximum_long_text_length, :allow_nil => true, :allow_blank => true
  
  named_scope :with_comments, :include => [:submission_comments ]
  named_scope :after, lambda{|date|
    {:conditions => ['submissions.created_at > ?', date] }
  }
  named_scope :before, lambda{|date|
    {:conditions => ['submissions.created_at < ?', date]}
  }
  named_scope :submitted_before, lambda{|date|
    {:conditions => ['submitted_at < ?', date] }
  }
  named_scope :submitted_after, lambda{|date|
    {:conditions => ['submitted_at > ?', date] }
  }
  
  named_scope :for_context_codes, lambda { |context_codes| { 
    :conditions => {:context_code => context_codes} } 
  }

  # This should only be used in the course drop down to show assignments recently graded.
  named_scope :recently_graded_assignments, lambda{|user_id, date, limit|
    {
            :select => 'assignments.id, assignments.title, assignments.points_possible, assignments.due_at,
                        submissions.grade, submissions.score, submissions.graded_at, assignments.grading_type,
                        assignments.context_id, assignments.context_type, courses.name AS context_name',
            :joins => 'JOIN assignments ON assignments.id = submissions.assignment_id
                       JOIN courses ON courses.id = assignments.context_id',
            :conditions => ["graded_at > ? AND user_id = ?", date.to_s(:db), user_id],
            :order => 'graded_at DESC',
            :limit => limit
            }
  }


  
  sanitize_field :body, Instructure::SanitizeField::SANITIZE
  
  before_save :update_if_pending
  before_save :validate_single_submission, :validate_enrollment, :infer_values, :set_context_code
  before_save :prep_for_submitting_to_turnitin
  before_save :check_url_changed
  after_save :touch_user
  after_save :update_assignment
  after_save :update_attachment_associations
  after_save :queue_websnap
  after_save :update_final_score
  after_save :submit_to_turnitin_later
  after_save :update_admins_if_just_submitted
  
  attr_reader :suppress_broadcast
  attr_reader :group_broadcast_submission

  adheres_to_policy
  
  has_a_broadcast_policy

  simply_versioned
  
  set_policy do
    given {|user| user && user.id == self.user_id }
    set {can :read and can :comment and can :make_group_comment and can :read_grade and can :submit }
    
    given {|user| self.assignment && self.assignment.context && user && self.user &&
      self.assignment.context.observer_enrollments.find_by_user_id_and_associated_user_id_and_workflow_state(user.id, self.user.id, 'active') }
    set {can :read and can :read_comments}
    
    given {|user, session| self.assignment.cached_context_grants_right?(user, session, :manage_grades) }#admins.include?(user) }
    set {can :read and can :comment and can :make_group_comment and can :read_grade and can :grade }
    
    given {|user| user && self.assessment_requests.map{|a| a.assessor_id}.include?(user.id) }
    set {can :read and can :comment}
  end
  
  on_update_send_to_streams do
    if self.graded_at && self.graded_at > 5.minutes.ago && !@already_sent_to_stream
      @already_sent_to_stream = true
      self.user_id
    end
  end
  
  def update_final_score
    Enrollment.send_later_if_production(:recompute_final_grade, self.user_id, self.context.id) if @score_changed
    self.assignment.send_later_if_production(:multiple_module_actions, [self.user_id], :scored, self.score) if self.assignment && @score_changed
    true
  end
  
  def url
    read_body = read_attribute(:body) && CGI::unescapeHTML(read_attribute(:body))
    if read_body && read_attribute(:url) && read_body[0..250] == read_attribute(:url)[0..250]
      @full_url = read_attribute(:body)
    else
      @full_url = read_attribute(:url)
    end
  end

  validates_each(:url, :allow_nil => true) do |record, attr, value|
    begin
      uri = URI.parse(value)
      raise(ArgumentError) if uri.scheme && !%w(http https).include?(uri.scheme)
      record.url = "http://#{value}" unless uri.scheme
    rescue URI::InvalidURIError, ArgumentError
      record.errors.add attr, 'is not a valid URL'
    end
  end

  def plaintext_body
    self.extend TextHelper
    strip_tags((self.body || "").gsub(/\<\s*br\s*\/\>/, "\n<br/>").gsub(/\<\/p\>/, "</p>\n"))
  end
  
  def check_turnitin_status(asset_string, attempt=1)
    self.turnitin_data ||= {}
    data = self.turnitin_data[asset_string]
    return unless data
    if !data[:similarity_score] && attempt < 10
      turnitin = Turnitin::Client.new(*self.context.turnitin_settings)
      res = turnitin.generateReport(self, asset_string)
      if res[:similarity_score]
        data[:similarity_score] = res[:similarity_score].to_f
        data[:web_overlap] = res[:web_overlap].to_f
        data[:publication_overlap] = res[:publication_overlap].to_f
        data[:student_overlap] = res[:student_overlap].to_f
        data[:state] = 'failure'
        data[:state] = 'problem' if data[:similarity_score] < 75
        data[:state] = 'warning' if data[:similarity_score] < 50
        data[:state] = 'acceptable' if data[:similarity_score] < 25
        data[:state] = 'none' if data[:similarity_score] == 0
      else
        send_at((5 * attempt).minutes.from_now, :check_turnitin_status, asset_string, attempt + 1)
      end
    else
      data[:error] = true
    end
    self.turnitin_data[asset_string] = data
    self.turnitin_data_changed!
    self.save
    data
  end
  
  def turnitin_report_url(asset_string, user)
    if self.turnitin_data && self.turnitin_data[asset_string] && self.turnitin_data[asset_string][:similarity_score]
      turnitin = Turnitin::Client.new(*self.context.turnitin_settings)
      if self.grants_right?(user, nil, :grade)
        turnitin.submissionReportUrl(self, asset_string)
      elsif self.current_submission_graded?
        turnitin.submissionStudentReportUrl(self, asset_string)
      end
    else
      nil
    end<commit_msg>update cached turnitin data when redirecting to originality report<commit_after>#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas 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, version 3 of the License.
#
# Canvas 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 <http://www.gnu.org/licenses/>.
#

class Submission < ActiveRecord::Base
  include SendToStream
  attr_protected :submitted_at
  attr_readonly :assignment_id
  belongs_to :attachment # this refers to the screenshot of the submission if it is a url submission
  belongs_to :assignment
  belongs_to :user
  belongs_to :grader, :class_name => 'User'
  belongs_to :group
  belongs_to :media_object
  belongs_to :student, :class_name => 'User', :foreign_key => :user_id
  has_many :submission_comments, :order => 'created_at', :dependent => :destroy
  has_many :assessment_requests, :as => :asset
  has_many :assigned_assessments, :class_name => 'AssessmentRequest', :as => :assessor_asset
  belongs_to :quiz_submission
  has_one :rubric_assessment, :as => :artifact, :conditions => {:assessment_type => "grading"}
  has_many :rubric_assessments, :as => :artifact
  has_many :attachment_associations, :as => :context
  has_many :attachments, :through => :attachment_associations
  serialize :turnitin_data, Hash
  validates_presence_of :assignment_id, :user_id
  validates_length_of :body, :maximum => maximum_long_text_length, :allow_nil => true, :allow_blank => true
  
  named_scope :with_comments, :include => [:submission_comments ]
  named_scope :after, lambda{|date|
    {:conditions => ['submissions.created_at > ?', date] }
  }
  named_scope :before, lambda{|date|
    {:conditions => ['submissions.created_at < ?', date]}
  }
  named_scope :submitted_before, lambda{|date|
    {:conditions => ['submitted_at < ?', date] }
  }
  named_scope :submitted_after, lambda{|date|
    {:conditions => ['submitted_at > ?', date] }
  }
  
  named_scope :for_context_codes, lambda { |context_codes| { 
    :conditions => {:context_code => context_codes} } 
  }

  # This should only be used in the course drop down to show assignments recently graded.
  named_scope :recently_graded_assignments, lambda{|user_id, date, limit|
    {
            :select => 'assignments.id, assignments.title, assignments.points_possible, assignments.due_at,
                        submissions.grade, submissions.score, submissions.graded_at, assignments.grading_type,
                        assignments.context_id, assignments.context_type, courses.name AS context_name',
            :joins => 'JOIN assignments ON assignments.id = submissions.assignment_id
                       JOIN courses ON courses.id = assignments.context_id',
            :conditions => ["graded_at > ? AND user_id = ?", date.to_s(:db), user_id],
            :order => 'graded_at DESC',
            :limit => limit
            }
  }


  
  sanitize_field :body, Instructure::SanitizeField::SANITIZE
  
  before_save :update_if_pending
  before_save :validate_single_submission, :validate_enrollment, :infer_values, :set_context_code
  before_save :prep_for_submitting_to_turnitin
  before_save :check_url_changed
  after_save :touch_user
  after_save :update_assignment
  after_save :update_attachment_associations
  after_save :queue_websnap
  after_save :update_final_score
  after_save :submit_to_turnitin_later
  after_save :update_admins_if_just_submitted
  
  attr_reader :suppress_broadcast
  attr_reader :group_broadcast_submission

  adheres_to_policy
  
  has_a_broadcast_policy

  simply_versioned
  
  set_policy do
    given {|user| user && user.id == self.user_id }
    set {can :read and can :comment and can :make_group_comment and can :read_grade and can :submit }
    
    given {|user| self.assignment && self.assignment.context && user && self.user &&
      self.assignment.context.observer_enrollments.find_by_user_id_and_associated_user_id_and_workflow_state(user.id, self.user.id, 'active') }
    set {can :read and can :read_comments}
    
    given {|user, session| self.assignment.cached_context_grants_right?(user, session, :manage_grades) }#admins.include?(user) }
    set {can :read and can :comment and can :make_group_comment and can :read_grade and can :grade }
    
    given {|user| user && self.assessment_requests.map{|a| a.assessor_id}.include?(user.id) }
    set {can :read and can :comment}
  end
  
  on_update_send_to_streams do
    if self.graded_at && self.graded_at > 5.minutes.ago && !@already_sent_to_stream
      @already_sent_to_stream = true
      self.user_id
    end
  end
  
  def update_final_score
    Enrollment.send_later_if_production(:recompute_final_grade, self.user_id, self.context.id) if @score_changed
    self.assignment.send_later_if_production(:multiple_module_actions, [self.user_id], :scored, self.score) if self.assignment && @score_changed
    true
  end
  
  def url
    read_body = read_attribute(:body) && CGI::unescapeHTML(read_attribute(:body))
    if read_body && read_attribute(:url) && read_body[0..250] == read_attribute(:url)[0..250]
      @full_url = read_attribute(:body)
    else
      @full_url = read_attribute(:url)
    end
  end

  validates_each(:url, :allow_nil => true) do |record, attr, value|
    begin
      uri = URI.parse(value)
      raise(ArgumentError) if uri.scheme && !%w(http https).include?(uri.scheme)
      record.url = "http://#{value}" unless uri.scheme
    rescue URI::InvalidURIError, ArgumentError
      record.errors.add attr, 'is not a valid URL'
    end
  end

  def plaintext_body
    self.extend TextHelper
    strip_tags((self.body || "").gsub(/\<\s*br\s*\/\>/, "\n<br/>").gsub(/\<\/p\>/, "</p>\n"))
  end
  
  def check_turnitin_status(asset_string, attempt=1)
    self.turnitin_data ||= {}
    data = self.turnitin_data[asset_string]
    return unless data
    if !data[:similarity_score] && attempt < 10
      turnitin = Turnitin::Client.new(*self.context.turnitin_settings)
      res = turnitin.generateReport(self, asset_string)
      if res[:similarity_score]
        data[:similarity_score] = res[:similarity_score].to_f
        data[:web_overlap] = res[:web_overlap].to_f
        data[:publication_overlap] = res[:publication_overlap].to_f
        data[:student_overlap] = res[:student_overlap].to_f
        data[:state] = 'failure'
        data[:state] = 'problem' if data[:similarity_score] < 75
        data[:state] = 'warning' if data[:similarity_score] < 50
        data[:state] = 'acceptable' if data[:similarity_score] < 25
        data[:state] = 'none' if data[:similarity_score] == 0
      else
        send_at((5 * attempt).minutes.from_now, :check_turnitin_status, asset_string, attempt + 1)
      end
    else
      data[:error] = true
    end
    self.turnitin_data[asset_string] = data
    self.turnitin_data_changed!
    self.save
    data
  end
  
  def turnitin_report_url(asset_string, user)
    if self.turnitin_data && self.turnitin_data[asset_string] && self.turnitin_data[asset_string][:similarity_score]
      turnitin = Turnitin::Client.new(*self.context.turnitin_settings)
      self.send_later(:check_turnitin_status, asset_string)
      if self.grants_right?(user, nil, :grade)
        turnitin.submissionReportUrl(self, asset_string)
      elsif self.current_submission_graded?
        turnitin.submissionStudentReportUrl(self, asset_string)
      end
    else
      nil
    end
{'fraction_non_alphanumeric': 0.10663308144416457, 'fraction_numerical': 0.0033585222502099076, 'mean_word_length': 3.962820512820513, 'pattern_counts': {'":': 0, '<': 27, '<?xml version=': 0, '>': 81, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5736262', 'n_tokens_mistral': 5090, 'n_tokens_neox': 4912, 'n_words': 1151}