<commit_before># == Schema Information
#
# Table name: courses
#
#  id                :integer          not null, primary key
#  title             :string(255)
#  created_at        :datetime
#  updated_at        :datetime
#  start             :date
#  end               :date
#  school            :string(255)
#  term              :string(255)
#  character_sum     :integer          default(0)
#  view_sum          :integer          default(0)
#  user_count        :integer          default(0)
#  article_count     :integer          default(0)
#  revision_count    :integer          default(0)
#  slug              :string(255)
#  listed            :boolean          default(TRUE)
#  untrained_count   :integer          default(0)
#  meeting_days      :string(255)
#  signup_token      :string(255)
#  assignment_source :string(255)
#  subject           :string(255)
#  expected_students :integer
#  description       :text
#  submitted         :boolean          default(FALSE)
#  passcode          :string(255)
#  timeline_start    :date
#  timeline_end      :date
#  day_exceptions    :string(255)      default("")
#  weekdays          :string(255)      default("0000000")
#  new_article_count :integer
#

require "#{Rails.root}/lib/importers/course_importer"
require "#{Rails.root}/lib/importers/user_importer"

#= Course model
class Course < ActiveRecord::Base
  LEGACY_COURSE_MAX_ID = 9999

  has_many :tags
  has_many :courses_users, class_name: CoursesUsers
  has_many :users, -> { uniq }, through: :courses_users,
                                after_remove: :cleanup_articles
  has_many :students, -> { where('courses_users.role = 0') },
           through: :courses_users, source: :user
  has_many :instructors, -> { where('courses_users.role = 1') },
           through: :courses_users, source: :user
  has_many :volunteers, -> { where('courses_users.role > 1') },
           through: :courses_users, source: :user

  has_many :revisions, -> (course) {
    where('date >= ?', course.start).where('date <= ?', course.end)
  }, through: :students

  has_many :cohorts_courses, class_name: CohortsCourses
  has_many :cohorts, through: :cohorts_courses

  has_many :articles_courses, class_name: ArticlesCourses
  has_many :articles, -> { uniq }, through: :articles_courses

  has_many :uploads, through: :students

  has_many :assignments

  has_many :weeks
  has_many :blocks, through: :weeks
  has_many :gradeables, as: :gradeable_item

  scope :current, lambda {
    current_and_future.where('start < ?', Time.now)
  }

  # A course stays "current" for a while after the end date, during which time
  # we still check for new data and update page views.
  scope :current_and_future, lambda {
    update_length = Figaro.env.update_length.to_i.days.seconds.to_i
    where('end > ?', Time.now - update_length)
  }

  # Courses sourced from Wikipedia, not created with this tool
  scope :legacy, -> { where('courses.id < 10000') }

  before_save :order_weeks

  validates :passcode, presence: true, unless: :is_legacy_course?

  ####################
  # Instance methods #
  ####################
  def to_param
    # This method is used by ActiveRecord
    slug
  end

  def wiki_title
    # Legacy courses using the EducationProgram extension have ids under 10000.
    prefix = id < 10000 ? 'Education_Program:' : Figaro.env.course_prefix + '/'
    escaped_slug = slug.gsub(' ', '_')
    "#{prefix}#{escaped_slug}"
  end

  def url
    language = Figaro.env.wiki_language
    "https://#{language}.wikipedia.org/wiki/#{wiki_title}"
  end

  def delist
    self.listed = false
    save
  end

  def update(data={}, save=true)
    if data.blank?
      data = CourseImporter.get_course_info id
      return if data.blank? || data[0].nil?
      data = data[0]
    end
    self.attributes = data['course']

    return unless save
    if data['participants']
      data['participants'].each_with_index do |(r, _p), i|
        UserImporter.add_users(data['participants'][r], i, self)
      end
    end
    self.save
  end

  def instructor_students
    students.where(id: students.collect(&:id) & instructors.collect(&:id))
  end

  def students_without_instructor_students
    students - instructor_students
  end

  #################
  # Cache methods #
  #################
  def character_sum
    update_cache unless self[:character_sum]
    self[:character_sum]
  end

  def view_sum
    update_cache unless self[:view_sum]
    self[:view_sum]
  end

  def user_count
    self[:user_count] || students_without_instructor_students.size
  end

  def untrained_count
    update_cache unless self[:untrained_count]
    self[:untrained_count]
  end

  def revision_count
    self[:revision_count] || revisions.size
  end

  def article_count
    self[:article_count] || articles.live.size
  end

  def new_article_count
    self[:new_article_count] || articles_courses.live.new_article.size
  end

  def update_cache
    # Do not consider revisions with negative byte changes
    self.character_sum = courses_users.where(role: 0).sum(:character_sum_ms)
    self.view_sum = articles_courses.live.sum(:view_count)
    self.user_count = students_without_instructor_students.size
    self.untrained_count = users.role('student').where(trained: false).size
    self.revision_count = revisions.size
    self.article_count = articles.live.size
    self.new_article_count = articles_courses.live.new_article.size
    save
  end

  def manual_update<commit_msg>Make explicit that Course#article_count and Course#new_article_count are mainspace articles only<commit_after># == Schema Information
#
# Table name: courses
#
#  id                :integer          not null, primary key
#  title             :string(255)
#  created_at        :datetime
#  updated_at        :datetime
#  start             :date
#  end               :date
#  school            :string(255)
#  term              :string(255)
#  character_sum     :integer          default(0)
#  view_sum          :integer          default(0)
#  user_count        :integer          default(0)
#  article_count     :integer          default(0)
#  revision_count    :integer          default(0)
#  slug              :string(255)
#  listed            :boolean          default(TRUE)
#  untrained_count   :integer          default(0)
#  meeting_days      :string(255)
#  signup_token      :string(255)
#  assignment_source :string(255)
#  subject           :string(255)
#  expected_students :integer
#  description       :text
#  submitted         :boolean          default(FALSE)
#  passcode          :string(255)
#  timeline_start    :date
#  timeline_end      :date
#  day_exceptions    :string(255)      default("")
#  weekdays          :string(255)      default("0000000")
#  new_article_count :integer
#

require "#{Rails.root}/lib/importers/course_importer"
require "#{Rails.root}/lib/importers/user_importer"

#= Course model
class Course < ActiveRecord::Base
  LEGACY_COURSE_MAX_ID = 9999

  has_many :tags
  has_many :courses_users, class_name: CoursesUsers
  has_many :users, -> { uniq }, through: :courses_users,
                                after_remove: :cleanup_articles
  has_many :students, -> { where('courses_users.role = 0') },
           through: :courses_users, source: :user
  has_many :instructors, -> { where('courses_users.role = 1') },
           through: :courses_users, source: :user
  has_many :volunteers, -> { where('courses_users.role > 1') },
           through: :courses_users, source: :user

  has_many :revisions, -> (course) {
    where('date >= ?', course.start).where('date <= ?', course.end)
  }, through: :students

  has_many :cohorts_courses, class_name: CohortsCourses
  has_many :cohorts, through: :cohorts_courses

  has_many :articles_courses, class_name: ArticlesCourses
  has_many :articles, -> { uniq }, through: :articles_courses

  has_many :uploads, through: :students

  has_many :assignments

  has_many :weeks
  has_many :blocks, through: :weeks
  has_many :gradeables, as: :gradeable_item

  scope :current, lambda {
    current_and_future.where('start < ?', Time.now)
  }

  # A course stays "current" for a while after the end date, during which time
  # we still check for new data and update page views.
  scope :current_and_future, lambda {
    update_length = Figaro.env.update_length.to_i.days.seconds.to_i
    where('end > ?', Time.now - update_length)
  }

  # Courses sourced from Wikipedia, not created with this tool
  scope :legacy, -> { where('courses.id < 10000') }

  before_save :order_weeks

  validates :passcode, presence: true, unless: :is_legacy_course?

  ####################
  # Instance methods #
  ####################
  def to_param
    # This method is used by ActiveRecord
    slug
  end

  def wiki_title
    # Legacy courses using the EducationProgram extension have ids under 10000.
    prefix = id < 10000 ? 'Education_Program:' : Figaro.env.course_prefix + '/'
    escaped_slug = slug.gsub(' ', '_')
    "#{prefix}#{escaped_slug}"
  end

  def url
    language = Figaro.env.wiki_language
    "https://#{language}.wikipedia.org/wiki/#{wiki_title}"
  end

  def delist
    self.listed = false
    save
  end

  def update(data={}, save=true)
    if data.blank?
      data = CourseImporter.get_course_info id
      return if data.blank? || data[0].nil?
      data = data[0]
    end
    self.attributes = data['course']

    return unless save
    if data['participants']
      data['participants'].each_with_index do |(r, _p), i|
        UserImporter.add_users(data['participants'][r], i, self)
      end
    end
    self.save
  end

  def instructor_students
    students.where(id: students.collect(&:id) & instructors.collect(&:id))
  end

  def students_without_instructor_students
    students - instructor_students
  end

  #################
  # Cache methods #
  #################
  def character_sum
    update_cache unless self[:character_sum]
    self[:character_sum]
  end

  def view_sum
    update_cache unless self[:view_sum]
    self[:view_sum]
  end

  def user_count
    self[:user_count] || students_without_instructor_students.size
  end

  def untrained_count
    update_cache unless self[:untrained_count]
    self[:untrained_count]
  end

  def revision_count
    self[:revision_count] || revisions.size
  end

  def article_count
    self[:article_count] || articles.namespace(0).live.size
  end

  def new_article_count
    self[:new_article_count] || articles_courses.live.new_article
      .joins(:article).where('articles.namespace = 0')
      .size
  end

  def update_cache
    # Do not consider revisions with negative byte changes
    self.character_sum = courses_users.where(role: 0).sum(:character_sum_ms)
    self.view_sum = articles_courses.live.sum(:view_count)
    self.user_count = students_without_instructor_students.size
    self.untrained_count = users.role('student').where(trained: false).size
    self.revision_count = revisions.size
    self.article_count = articles.namespace(0).live.size
    self.new_article_count = articles_courses.live.new_article
      .joins(:article).where('articles.namespace = 0')
      .size
    save
  end

  def manual_update
{'fraction_non_alphanumeric': 0.11511178953039418, 'fraction_numerical': 0.013109454969920087, 'mean_word_length': 3.047238372093023, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 23, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, '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': '4185364', 'n_tokens_mistral': 3695, 'n_tokens_neox': 3476, 'n_words': 790}