添付ファイルの成績と成績のパスバック

これは、Classroom アドオンのチュートリアル シリーズの6 つ目のチュートリアルです。

このチュートリアルでは、前のチュートリアルのステップの例を変更して、採点対象のアクティビティ タイプのアタッチメントを作成します。また、プログラムで成績を Google Classroom に渡すこともできます。この成績は、教師の採点簿に下書きの成績として表示されます。

このチュートリアルは、他の手順とは若干異なり、Classroom に成績を返す方法が 2 つ考えられます。どちらも、デベロッパーとユーザーの両方のエクスペリエンスに明確な影響を与えます。Classroom アドオンを設計する際は、両方を検討してください。実装オプションについて詳しくは、添付ファイルの操作に関するガイドのページをご覧ください。

API の採点機能は省略可です。任意のアクティビティ タイプのアタッチメントで使用できます。

このチュートリアルでは、次のことを行います。

  • 添付ファイルの成績分母も設定するように、Classroom API に対する以前の添付ファイル作成リクエストを変更します。
  • 生徒の提出物をプログラムで採点し、添付ファイルの成績分子を設定する。
  • ログイン済みまたはオフラインの教師の認証情報を使用して、提出物の成績を Classroom に渡す 2 つの方法を実装します。

完了すると、パスバック動作がトリガーされた後、Classroom の採点簿に成績が表示されます。正確なタイミングは、実装方法によって異なります。

この例では、前のチュートリアルのアクティビティを再利用します。このアクティビティでは、有名なランドマークの画像が表示され、その名前を入力するよう求められます。受講生が正しい名前を入力した場合は添付ファイルに満点を与え、それ以外の場合は 0 点にします。

Classroom アドオン API の採点機能について

アドオンでは、アタッチメントの成績分子と成績分母の両方を設定できます。これらは、API の pointsEarned 値と maxPoints 値を使用してそれぞれ設定されます。maxPoints を設定すると、Classroom UI の添付ファイル カードにその値が表示されます。

1 つの割り当てに maxPoints を持つ複数のアタッチメントの例

図 1. maxPoints が設定された 3 つのアドオン添付カードを含む課題作成 UI。

Classroom アドオン API を使用すると、添付ファイルの成績の設定を構成し、獲得スコアを設定できます。これは、課題の成績とは異なります。ただし、課題の成績設定は、添付ファイル カードで [成績の同期] ラベルが付いている添付ファイルの成績設定に従います。「成績の同期」の添付ファイルで生徒の提出物に pointsEarned を設定すると、課題の生徒の仮成績も設定されます。

通常、maxPoints を設定する課題に追加された最初の添付ファイルには、「成績の同期」ラベルが付けられます。「成績の同期」ラベルの例については、図 1 の課題作成 UI の例をご覧ください。[添付ファイル 1] カードに [Grade sync](成績の同期)ラベルが付いており、赤いボックス内の課題の成績が 50 点に更新されていることに注目してください。また、図 1 には 3 つの添付カードが表示されていますが、[Grade sync] ラベルが付いているのは 1 枚のみです。これは現在の実装の重要な制限事項です。「成績の同期」ラベルを設定できるのは 1 つの添付ファイルのみです

maxPoints が設定されている添付ファイルが複数ある場合、[成績の同期] を使用して添付ファイルを削除しても、残りの添付ファイルで「成績の同期」が有効になりませんmaxPoints を設定する別のアタッチメントを追加すると、新しいアタッチメントで成績の同期が有効になり、課題の最大成績が調整されて一致します。プログラムで「成績の同期」ラベルが付いている添付ファイルを確認したり、特定の課題に添付されている添付ファイルの数を確認したりするためのメカニズムはありません。

添付ファイルに最高成績を設定する

このセクションでは、アタッチメントの成績の分母(すべての生徒が提出で獲得できる最高得点)の設定について説明します。そのためには、アタッチメントの maxPoints 値を設定します。

採点機能を有効にするには、既存の実装にわずかな変更を加えるだけで済みます。アタッチメントを作成するときに、studentWorkReviewUriteacherViewUri、その他の添付ファイル フィールドを含む同じ AddOnAttachment オブジェクトmaxPoints 値を追加します。

新しい課題のデフォルトの最大スコアは 100 です。成績が正しく設定されていることを確認できるように、maxPoints を 100 以外の値に設定することをおすすめします。デモとして、maxPoints を 50 に設定します。

Python

attachment オブジェクトを作成するときに、courses.courseWork.addOnAttachments エンドポイントCREATE リクエストを送信する直前に maxPoints フィールドを追加します。提供されている例に沿って作成した場合は、webapp/attachment_routes.py ファイルで確認できます。

attachment = {
    # Specifies the route for a teacher user.
    "teacherViewUri": {
        "uri":
            flask.url_for(
                "load_activity_attachment",
                _scheme='https',
                _external=True),
    },
    # Specifies the route for a student user.
    "studentViewUri": {
        "uri":
            flask.url_for(
                "load_activity_attachment",
                _scheme='https',
                _external=True)
    },
    # Specifies the route for a teacher user when the attachment is
    # loaded in the Classroom grading view.
    "studentWorkReviewUri": {
        "uri":
            flask.url_for(
                "view_submission", _scheme='https', _external=True)
    },
    # Sets the maximum points that a student can earn for this activity.
    # This is the denominator in a fractional representation of a grade.
    "maxPoints": 50,
    # The title of the attachment.
    "title": f"Attachment {attachment_count}",
}

このデモでは、maxPoints 値をローカルの添付ファイル データベースにも保存します。これにより、後で生徒の提出物を採点する際に追加の API 呼び出しを行う必要がなくなります。ただし、アドオンとは独立して、教師が課題の成績設定を変更する可能性はあります。courses.courseWork エンドポイントGET リクエストを送信して、割り当てレベルの maxPoints 値を確認します。その場合は、CourseWork.id フィールドに itemId を渡します。

次に、データベース モデルを更新して、アタッチメントの maxPoints 値も保持するようにします。CREATE レスポンスの maxPoints 値を使用することをおすすめします。

Python

まず、Attachment テーブルに max_points フィールドを追加します。提供されている例に沿って作成した場合は、webapp/models.py ファイルにあります。

# Database model to represent an attachment.
class Attachment(db.Model):
    # The attachmentId is the unique identifier for the attachment.
    attachment_id = db.Column(db.String(120), primary_key=True)

    # The image filename to store.
    image_filename = db.Column(db.String(120))

    # The image caption to store.
    image_caption = db.Column(db.String(120))

    # The maximum number of points for this activity.
    max_points = db.Column(db.Integer)

courses.courseWork.addOnAttachments CREATE リクエストに戻ります。レスポンスで返された maxPoints 値を保存します。

new_attachment = Attachment(
    # The new attachment's unique ID, returned in the CREATE response.
    attachment_id=resp.get("id"),
    image_filename=key,
    image_caption=value,
    # Store the maxPoints value returned in the response.
    max_points=int(resp.get("maxPoints")))
db.session.add(new_attachment)
db.session.commit()

添付ファイルに最大評価が設定されました。これで、この動作をテストできるようになりました。添付ファイルを新しい課題に追加すると、添付ファイル カードに [成績の同期] ラベルが表示され、課題の [点数] 値が変化することを確認します。

Classroom で生徒の提出物の成績を設定する

このセクションでは、添付ファイルの成績の分子(添付ファイルに対する個々の生徒のスコア)の設定について説明します。これを行うには、生徒の提出物の pointsEarned 値を設定します。

ここで重要な決定を下す必要があります。アドオンが pointsEarned を設定するためのリクエストを発行する方法です。

問題は、pointsEarned を設定するには teacher OAuth スコープが必要なことです。生徒のユーザーに teacher スコープを付与しないでください。これにより、生徒ビュー iframe ではなく Teacher View iframe を読み込むなど、生徒がアドオンを操作したときに予期しない動作が発生する可能性があります。したがって、pointsEarned の設定方法は 2 つあります。

  • ログインしている教師の認証情報を使用する。
  • 保存されている(オフラインの)教師の認証情報を使用する。

以降のセクションでは、各実装を説明する前に、各アプローチのトレードオフについて説明します。提供されている例では、成績を Classroom に渡すための両方のアプローチを示しています。提供されている例を実行する際にアプローチを選択する方法については、以下の言語固有の手順をご覧ください。

Python

webapp/attachment_routes.py ファイルの上部にある SET_GRADE_WITH_LOGGED_IN_USER_CREDENTIALS 宣言を見つけます。ログインした教師の認証情報を使用して成績を返すには、この値を True に設定します。生徒がアクティビティを送信するときに保存された認証情報を使用して成績を返すようにするには、この値を False に設定します。

ログインした教師の認証情報を使用して成績を設定する

ログインしているユーザーの認証情報を使用して、pointsEarned を設定するリクエストを発行します。これは、これまでの他の実装を反映しているため、直感的に理解でき、実現する労力はほとんど必要ありません。

ただし、教師は [生徒の提出物の確認] iframe で生徒の提出物にのみ操作します。これには重要な意味があります。

  • 教師が Classroom UI で操作を行うまで、Classroom に成績は入力されません。
  • すべての生徒の成績を入力するには、教師が生徒の提出物をすべて開く必要がある場合があります。
  • Classroom が成績を受信してから Classroom UI に表示されるまでに、少し時間がかかります。通常は 5 ~ 10 秒ですが、30 秒まで遅延することがあります。

これらの要因が組み合わさると、教師はクラスの成績をすべて入力するために、多大で時間のかかる手作業が必要になることがあります。

このアプローチを実装するには、既存の学生の課題のレビュー ルートに API 呼び出しを 1 つ追加します。

生徒の提出物と添付ファイルのレコードを取得したら、生徒の提出物を評価し、結果の成績を保存します。AddOnAttachmentStudentSubmission オブジェクトpointsEarned フィールドに成績を設定します。最後に、リクエスト本文に AddOnAttachmentStudentSubmission インスタンスを指定して、courses.courseWork.addOnAttachments.studentSubmissions エンドポイントPATCH リクエストを発行します。PATCH リクエストの updateMaskpointsEarned を指定する必要もあります。

Python

# Look up the student's submission in our database.
student_submission = Submission.query.get(flask.session["submissionId"])

# Look up the attachment in the database.
attachment = Attachment.query.get(student_submission.attachment_id)

grade = 0

# See if the student response matches the stored name.
if student_submission.student_response.lower(
) == attachment.image_caption.lower():
    grade = attachment.max_points

# Create an instance of the Classroom service.
classroom_service = ch._credential_handler.get_classroom_service()

# Build an AddOnAttachmentStudentSubmission instance.
add_on_attachment_student_submission = {
    # Specifies the student's score for this attachment.
    "pointsEarned": grade,
}

# Issue a PATCH request to set the grade numerator for this attachment.
patch_grade_response = classroom_service.courses().courseWork(
).addOnAttachments().studentSubmissions().patch(
    courseId=flask.session["courseId"],
    itemId=flask.session["itemId"],
    attachmentId=flask.session["attachmentId"],
    submissionId=flask.session["submissionId"],
    # updateMask is a list of fields being modified.
    updateMask="pointsEarned",
    body=add_on_attachment_student_submission).execute()

オフラインの教師認証情報を使用して成績を設定する

2 つ目のアプローチで成績を設定するには、添付ファイルを作成した教師の保存された認証情報を使用する必要があります。この実装では、事前に承認された教師のリフレッシュ トークンとアクセス トークンを使用して認証情報を作成し、これらの認証情報を使用して pointsEarned を設定する必要があります。

このアプローチの重要な利点は、教師の操作を必要とせずに成績が Classroom UI に入力されるため、上記の問題を回避できることです。その結果、エンドユーザーは採点作業をシームレスかつ効率的であると認識します。また、このアプローチでは、生徒がアクティビティを完了したときや非同期的に、成績を返すタイミングを選択できます。

このアプローチを実装するには、次のタスクを完了します。

  1. アクセス トークンを保存するようにユーザー データベースのレコードを変更する。
  2. 添付ファイルのデータベース レコードを変更して教師 ID を保存する。
  3. 教師の認証情報を取得し、必要に応じて新しい Classroom サービス インスタンスを作成します。
  4. 提出物の成績を設定します。

このデモでは、生徒がアクティビティを完了したとき(生徒が生徒ビューのルートでフォームを送信したとき)に成績を設定します。

アクセス トークンを保存するようにユーザー データベース レコードを変更する

API を呼び出すには、更新トークンとアクセス トークンという 2 つの一意のトークンが必要です。これまでの一連のチュートリアルに沿って操作してきた場合、User テーブル スキーマにはすでに更新トークンが格納されているはずです。認証フローの一部としてアクセス トークンを受け取るため、ログインしたユーザーでのみ API 呼び出しを行う場合は、更新トークンを保存するだけで十分です。

ただし、ログインしたユーザー以外のユーザーとして呼び出しを行う必要があるため、認証フローは使用できません。したがって、更新トークンとともにアクセス トークンを保存する必要があります。User テーブル スキーマを更新して、アクセス トークンを含めます。

Python

提供されている例では、webapp/models.py ファイルにあります。

# Database model to represent a user.
class User(db.Model):
    # The user's identifying information:
    id = db.Column(db.String(120), primary_key=True)
    display_name = db.Column(db.String(80))
    email = db.Column(db.String(120), unique=True)
    portrait_url = db.Column(db.Text())

    # The user's refresh token, which will be used to obtain an access token.
    # Note that refresh tokens will become invalid if:
    # - The refresh token has not been used for six months.
    # - The user revokes your app's access permissions.
    # - The user changes passwords.
    # - The user belongs to a Google Cloud organization
    #   that has session control policies in effect.
    refresh_token = db.Column(db.Text())

    # An access token for this user.
    access_token = db.Column(db.Text())

次に、User レコードを作成または更新するコードを更新して、アクセス トークンも格納します。

Python

提供されている例では、webapp/credential_handler.py ファイルにあります。

def save_credentials_to_storage(self, credentials):
    # Issue a request for the user's profile details.
    user_info_service = googleapiclient.discovery.build(
        serviceName="oauth2", version="v2", credentials=credentials)
    user_info = user_info_service.userinfo().get().execute()
    flask.session["username"] = user_info.get("name")
    flask.session["login_hint"] = user_info.get("id")

    # See if we have any stored credentials for this user. If they have used
    # the add-on before, we should have received login_hint in the query
    # parameters.
    existing_user = self.get_credentials_from_storage(user_info.get("id"))

    # If we do have stored credentials, update the database.
    if existing_user:
        if user_info:
            existing_user.id = user_info.get("id")
            existing_user.display_name = user_info.get("name")
            existing_user.email = user_info.get("email")
            existing_user.portrait_url = user_info.get("picture")

        if credentials and credentials.refresh_token is not None:
            existing_user.refresh_token = credentials.refresh_token
            # Update the access token.
            existing_user.access_token = credentials.token

    # If not, this must be a new user, so add a new entry to the database.
    else:
        new_user = User(
            id=user_info.get("id"),
            display_name=user_info.get("name"),
            email=user_info.get("email"),
            portrait_url=user_info.get("picture"),
            refresh_token=credentials.refresh_token,
            # Store the access token as well.
            access_token=credentials.token)

        db.session.add(new_user)

    db.session.commit()

添付ファイルのデータベース レコードを変更して教師 ID を保存する

アクティビティの成績を設定するには、pointsEarned をコースの教師として設定するように呼び出します。これを実現するには、いくつかの方法があります。

  • 教師の認証情報とコース ID のローカル マッピングを保存します。ただし、同じ教師が特定のコースと関連付けられているとは限りません。
  • Classroom API の courses エンドポイントGET リクエストを発行して、現在の教師を取得します。次に、ローカル ユーザー レコードをクエリして、一致する教師の認証情報を探します。
  • アドオン アタッチメントを作成するときに、ローカル アタッチメント データベースに教師 ID を保存します。次に、生徒ビューの iframe に渡された attachmentId から教師の認証情報を取得します。

この例では、最後のオプションについて説明します。これは、生徒がアクティビティの添付を完了したときに成績を設定するためです。

データベースの Attachment テーブルに教師 ID フィールドを追加します。

Python

提供されている例では、webapp/models.py ファイルにあります。

# Database model to represent an attachment.
class Attachment(db.Model):
    # The attachmentId is the unique identifier for the attachment.
    attachment_id = db.Column(db.String(120), primary_key=True)

    # The image filename to store.
    image_filename = db.Column(db.String(120))

    # The image caption to store.
    image_caption = db.Column(db.String(120))

    # The maximum number of points for this activity.
    max_points = db.Column(db.Integer)

    # The ID of the teacher that created the attachment.
    teacher_id = db.Column(db.String(120))

次に、Attachment レコードを作成または更新するコードを更新して、クリエイターの ID も保存します。

Python

提供されている例では、これは webapp/attachment_routes.py ファイルの create_attachments メソッド内にあります。

# Store the attachment by id.
new_attachment = Attachment(
    # The new attachment's unique ID, returned in the CREATE response.
    attachment_id=resp.get("id"),
    image_filename=key,
    image_caption=value,
    max_points=int(resp.get("maxPoints")),
    teacher_id=flask.session["login_hint"])
db.session.add(new_attachment)
db.session.commit()

教師の認証情報を取得する

生徒ビューの iframe を提供するルートを見つけます。生徒の回答をローカル データベースに保存したらすぐに、ローカル ストレージから教師の認証情報を取得します。前の 2 つのステップで準備をしていれば、簡単に設定できます。また、これらの情報を使用して、教師ユーザー用の Classroom サービスの新しいインスタンスを作成することもできます。

Python

提供されている例では、これは webapp/attachment_routes.py ファイルの load_activity_attachment メソッドにあります。

# Create an instance of the Classroom service using the tokens for the
# teacher that created the attachment.

# We're assuming that there are already credentials in the session, which
# should be true given that we are adding this within the Student View
# route; we must have had valid credentials for the student to reach this
# point. The student credentials will be valid to construct a Classroom
# service for another user except for the tokens.
if not flask.session.get("credentials"):
    raise ValueError(
        "No credentials found in session for the requested user.")

# Make a copy of the student credentials so we don't modify the original.
teacher_credentials_dict = deepcopy(flask.session.get("credentials"))

# Retrieve the requested user's stored record.
teacher_record = User.query.get(attachment.teacher_id)

# Apply the user's tokens to the copied credentials.
teacher_credentials_dict["refresh_token"] = teacher_record.refresh_token
teacher_credentials_dict["token"] = teacher_record.access_token

# Construct a temporary credentials object.
teacher_credentials = google.oauth2.credentials.Credentials(
    **teacher_credentials_dict)

# Refresh the credentials if necessary; we don't know when this teacher last
# made a call.
if teacher_credentials.expired:
    teacher_credentials.refresh(Request())

# Request the Classroom service for the specified user.
teacher_classroom_service = googleapiclient.discovery.build(
    serviceName=CLASSROOM_API_SERVICE_NAME,
    version=CLASSROOM_API_VERSION,
    credentials=teacher_credentials)

提出物の成績を設定する

この手順は、ログインしている教師の認証情報を使用する場合と同じです。ただし、前の手順で取得した教師の認証情報を使用して呼び出す必要があります。

Python

# Issue a PATCH request as the teacher to set the grade numerator for this
# attachment.
patch_grade_response = teacher_classroom_service.courses().courseWork(
).addOnAttachments().studentSubmissions().patch(
    courseId=flask.session["courseId"],
    itemId=flask.session["itemId"],
    attachmentId=flask.session["attachmentId"],
    submissionId=flask.session["submissionId"],
    # updateMask is a list of fields being modified.
    updateMask="pointsEarned",
    body=add_on_attachment_student_submission).execute()

アドオンをテストする

前のチュートリアルと同様に、教師としてアクティビティ タイプの添付ファイルを含む課題を作成し、生徒として回答を送信してから、[生徒の課題の確認] iframe で提出内容を開きます。実装方法に応じて、成績が異なるタイミングで表示されます。

  • 生徒がアクティビティを完了したときに成績を返却するように選択した場合は、生徒の課題の確認の iframe を開く前に、UI に下書きの成績が表示されているはずです。また、課題を開いた生徒リストや、[Student Work Review] iframe の横にある [採点] ボックスでも確認できます。
  • 教師が [生徒の課題の確認] iframe を開いたときに成績を返すように選択した場合、iframe の読み込み直後に [成績] ボックスに成績が表示されます。上記のように、この処理には 30 秒ほどかかることがあります。 その後、特定の生徒の成績が他の Classroom 採点簿ビューにも表示されます。

生徒に正しいスコアが表示されていることを確認します。

これで、次のステップである Google Classroom の外部で添付ファイルを作成するに進みます。