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

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

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

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

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

このチュートリアルでは、次のことを完了します。

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

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

この例では、前のチュートリアルのアクティビティを再利用します。ここでは、生徒に有名なランドマークの画像が表示され、名前の入力を求められます。学生が正しい名前を入力した場合は、添付ファイルに満点を割り当てます。それ以外の場合は、ゼロを割り当てます。

Classroom アドオン API の採点機能について理解する

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

1 つの割り当てに maxPoints がある複数のアタッチメントの例

図 1. maxPoints が設定された 3 つのアドオン添付ファイル カードが表示された課題作成 UI。

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

通常、maxPoints を設定する課題に追加された最初の添付ファイルには「成績の同期」ラベルが付けられます。「成績の同期」ラベルの例については、図 1 の課題作成 UI の例をご覧ください。「添付ファイル 1」カードには「成績の同期」ラベルがあり、赤いボックス内の課題の成績が 50 点に更新されています。また、図 1 には 3 つの添付ファイル カードが示されていますが、「成績の同期」ラベルが付いているカードは 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 呼び出しを行う必要がなくなります。ただし、教師が課題の成績設定をアドオンとは別に変更することは可能です。GET リクエストを courses.courseWork エンドポイントに送信して、割り当てレベルの maxPoints 値を確認します。その際は、itemIdCourseWork.id フィールドに渡します。

次に、アタッチメントの 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()

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

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

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

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

問題は、pointsEarned の設定に teacher OAuth スコープが必要であることです。teacher スコープを生徒のユーザーに付与しないでください。生徒がアドオンを操作(生徒ビューの iframe ではなく教師ビューの 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()

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

Student View 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,
    discoveryServiceUrl=f"https://classroom.googleapis.com/$discovery/rest?labels=ADD_ONS_ALPHA&key={GOOGLE_API_KEY}",
    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 に仮成績がすでに表示されているはずです。また、課題を開いた生徒リストや、[生徒の提出物] iframe の横にある [採点] ボックスでも確認できます。
  • 教師が生徒の提出物レビューの iframe を開いたときに成績を返すよう選択した場合は、iframe が読み込まれるとすぐに [成績] ボックスに成績が表示されます。上記のとおり、これには 30 秒ほどかかることがあります。 その後、Classroom の採点簿の他のビューにも特定の生徒の成績が表示されます。

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

これで完了です。次のステップ(Google Classroom の外部で添付ファイルを作成する)に進むことができます。