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

これは、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()

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

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 秒になることもあります。

これらの要因が組み合わさると、クラスの成績を完全に反映するために、教師は多大な時間のかかる手作業を必要とする可能性があります。

このアプローチを実装するには、既存の Student Work Review ルートに API 呼び出しをもう 1 つ追加します。

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

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 をローカル添付ファイル データベースに保存します。次に、Student View 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 に仮成績が表示されているはずです。また、課題を開いた生徒リストや、[Student Work Review] iframe の横にある [採点] ボックスでも確認できます。
  • 教師が生徒の提出物の確認の iframe を開いたときに成績を返すよう選択した場合は、iframe が読み込まれるとすぐに [成績] ボックスに成績が表示されます。上記のように、この処理には 30 秒ほどかかることがあります。 その後、Classroom の採点簿の他のビューには、特定の生徒の成績も表示されます。

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

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