chromium/chrome/browser/ui/ash/glanceables/glanceables_classroom_course_work_item.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/ash/glanceables/glanceables_classroom_course_work_item.h"

#include <optional>

#include "ash/glanceables/classroom/glanceables_classroom_types.h"
#include "base/functional/callback.h"
#include "base/time/time.h"
#include "google_apis/classroom/classroom_api_course_work_response_types.h"
#include "google_apis/classroom/classroom_api_student_submissions_response_types.h"

namespace ash {
namespace {

bool TimesWithinDelta(const base::Time& lhs,
                      const base::Time& rhs,
                      const base::TimeDelta& max_distance) {
  return (lhs - rhs).magnitude() <= max_distance;
}

std::optional<base::Time> ConvertCourseWorkItemDue(
    const std::optional<google_apis::classroom::CourseWorkItem::DueDateTime>&
        raw_due) {
  if (!raw_due.has_value()) {
    return std::nullopt;
  }

  const base::Time::Exploded exploded_due = {.year = raw_due->year,
                                             .month = raw_due->month,
                                             .day_of_month = raw_due->day};
  base::Time due;
  if (!base::Time::FromUTCExploded(exploded_due, &due)) {
    return std::nullopt;
  }
  return due + raw_due->time_of_day;
}

GlanceablesClassroomStudentSubmissionState CalculateStudentSubmissionState(
    const google_apis::classroom::StudentSubmission* raw_student_submission) {
  const auto raw_state = raw_student_submission->state();
  if (raw_state == google_apis::classroom::StudentSubmission::State::kNew ||
      raw_state == google_apis::classroom::StudentSubmission::State::kCreated ||
      raw_state == google_apis::classroom::StudentSubmission::State::
                       kReclaimedByStudent) {
    return GlanceablesClassroomStudentSubmissionState::kAssigned;
  }

  if (raw_state ==
      google_apis::classroom::StudentSubmission::State::kTurnedIn) {
    return GlanceablesClassroomStudentSubmissionState::kTurnedIn;
  }

  if (raw_state ==
      google_apis::classroom::StudentSubmission::State::kReturned) {
    return raw_student_submission->assigned_grade().has_value()
               ? GlanceablesClassroomStudentSubmissionState::kGraded
               : GlanceablesClassroomStudentSubmissionState::kAssigned;
  }

  return GlanceablesClassroomStudentSubmissionState::kOther;
}

}  // namespace

GlanceablesClassroomCourseWorkItem::GlanceablesClassroomCourseWorkItem() =
    default;

GlanceablesClassroomCourseWorkItem::GlanceablesClassroomCourseWorkItem(
    const GlanceablesClassroomCourseWorkItem&) = default;

GlanceablesClassroomCourseWorkItem&
GlanceablesClassroomCourseWorkItem::operator=(
    const GlanceablesClassroomCourseWorkItem&) = default;

GlanceablesClassroomCourseWorkItem::~GlanceablesClassroomCourseWorkItem() =
    default;

void GlanceablesClassroomCourseWorkItem::SetCourseWorkItem(
    const google_apis::classroom::CourseWorkItem* course_work) {
  course_work_item_set_ = true;
  can_course_work_item_be_revalidated_ = false;

  title_ = course_work->title();
  link_ = course_work->alternate_link();
  due_ = ConvertCourseWorkItemDue(course_work->due_date_time());
  creation_time_ = course_work->creation_time();
  last_update_ = course_work->last_update();
}

void GlanceablesClassroomCourseWorkItem::AddStudentSubmission(
    const google_apis::classroom::StudentSubmission* submission) {
  ++current_submissions_state_.total_count;
  if (submission->last_update().has_value() &&
      submission->last_update() > most_recent_submission_update_time_) {
    most_recent_submission_update_time_ = submission->last_update().value();
  }

  switch (CalculateStudentSubmissionState(submission)) {
    case GlanceablesClassroomStudentSubmissionState::kGraded:
      ++current_submissions_state_.number_turned_in;
      ++current_submissions_state_.number_graded;
      break;
    case GlanceablesClassroomStudentSubmissionState::kTurnedIn:
      ++current_submissions_state_.number_turned_in;
      break;
    case GlanceablesClassroomStudentSubmissionState::kAssigned:
    case GlanceablesClassroomStudentSubmissionState::kOther:
      break;
  }
}

void GlanceablesClassroomCourseWorkItem::InvalidateStudentSubmissions() {
  previous_submissions_state_ = current_submissions_state_;

  current_submissions_state_.Reset();
}

void GlanceablesClassroomCourseWorkItem::RestorePreviousStudentSubmissions() {
  if (!previous_submissions_state_) {
    return;
  }

  current_submissions_state_ = *previous_submissions_state_;
  previous_submissions_state_.reset();
}

void GlanceablesClassroomCourseWorkItem::InvalidateCourseWorkItem() {
  can_course_work_item_be_revalidated_ = course_work_item_set_;
  course_work_item_set_ = false;
}

void GlanceablesClassroomCourseWorkItem::RevalidateCourseWorkItem() {
  if (can_course_work_item_be_revalidated_) {
    course_work_item_set_ = true;
  }
}

bool GlanceablesClassroomCourseWorkItem::SatisfiesPredicates(
    base::RepeatingCallback<bool(const std::optional<base::Time>&)>
        due_predicate,
    base::RepeatingCallback<bool(GlanceablesClassroomStudentSubmissionState)>
        submission_state_predicate) const {
  if (!IsValid()) {
    return false;
  }

  if (!due_predicate.Run(due_)) {
    return false;
  }

  GlanceablesClassroomStudentSubmissionState effective_state =
      GlanceablesClassroomStudentSubmissionState::kAssigned;
  if (total_submissions() == graded_submissions()) {
    effective_state = GlanceablesClassroomStudentSubmissionState::kGraded;
  } else if (total_submissions() == turned_in_submissions()) {
    effective_state = GlanceablesClassroomStudentSubmissionState::kTurnedIn;
  }

  return submission_state_predicate.Run(effective_state);
}

std::unique_ptr<GlanceablesClassroomAssignment>
GlanceablesClassroomCourseWorkItem::CreateClassroomAssignment(
    const std::string& course_name,
    bool include_aggregated_submissions_state) const {
  CHECK(IsValid());

  std::optional<GlanceablesClassroomAggregatedSubmissionsState>
      aggregated_submissions_state;
  if (include_aggregated_submissions_state) {
    aggregated_submissions_state = current_submissions_state_;
  }
  return std::make_unique<GlanceablesClassroomAssignment>(
      course_name, title_, link_, due_, last_update_,
      aggregated_submissions_state);
}

bool GlanceablesClassroomCourseWorkItem::IsValid() const {
  return course_work_item_set_ && total_submissions() > 0;
}

bool GlanceablesClassroomCourseWorkItem::StudentSubmissionsNeedRefetch(
    const base::Time& now) const {
  if (last_submissions_fetch_.is_null()) {
    return true;
  }

  if (last_update_ > last_submissions_fetch_) {
    return true;
  }

  const base::TimeDelta time_from_last_refresh = now - last_submissions_fetch_;

  if (TimesWithinDelta(last_update_, now, base::Days(2))) {
    return true;
  }

  if (TimesWithinDelta(last_update_, now, base::Days(7)) &&
      time_from_last_refresh > base::Days(1)) {
    return true;
  }

  if (graded_submissions() < total_submissions() &&
      time_from_last_refresh > base::Days(7)) {
    return true;
  }

  if (due_) {
    // Course work with due date within a day to now is likely to be shown in
    // the UI - refresh it.
    if (TimesWithinDelta(*due_, now, base::Days(2))) {
      return true;
    }

    // If due date is within few days, refresh it if the student submissions
    // have not been updated recently.
    if (TimesWithinDelta(*due_, now, base::Days(5))) {
      return time_from_last_refresh > base::Hours(12);
    }

    if (TimesWithinDelta(*due_, now, base::Days(14))) {
      return time_from_last_refresh > base::Days(1);
    }
  }

  return false;
}

void GlanceablesClassroomCourseWorkItem::SetHasFreshSubmissionsState(
    bool value,
    const base::Time& now) {
  has_fresh_submissions_state_ = value;
  if (has_fresh_submissions_state_) {
    previous_submissions_state_.reset();
    last_submissions_fetch_ = now;
  }
}

}  // namespace ash