これは、Classroom アドオンのチュートリアル シリーズの 6 番目のチュートリアルです。
このチュートリアルでは、前のチュートリアル ステップの例を変更して、採点済みのアクティビティ タイプ添付ファイルを作成します。また、成績を Google Classroom にプログラムで渡すこともできます。この成績は、教師の採点簿に下書きの成績として表示されます。
このチュートリアルは、合格した成績を Classroom に返す方法が 2 つあるという点で、このシリーズの他のチュートリアルとは若干異なります。どちらもデベロッパーとユーザーのエクスペリエンスに大きな影響を与えます。Classroom アドオンを設計する際は、両方を考慮してください。実装オプションの詳細については、添付ファイルの操作に関するガイドページをご覧ください。
API の採点機能は省略可能です。これらは、任意のアクティビティ タイプのアタッチメントで使用できます。
このチュートリアルでは、次のことを行います。
- Classroom API への以前の添付ファイル作成リクエストを変更して、添付ファイルの成績の分母も設定します。
- 生徒の提出物をプログラムで採点し、添付ファイルの成績の分子を設定します。
- ログインした教師の認証情報またはオフラインの教師の認証情報を使用して、課題の成績を Classroom に渡す 2 つの方法を実装します。
完了すると、パスバック動作がトリガーされた後、成績が Classroom の採点簿に表示されます。この処理が行われる正確なタイミングは、実装アプローチによって異なります。
この例では、前のチュートリアルで作成したアクティビティを再利用します。このアクティビティでは、有名なランドマークの画像が学生に表示され、その名前を入力するよう求められます。生徒が正しい名前を入力した場合は添付ファイルに満点を割り当て、それ以外の場合は 0 点を割り当てます。
Classroom アドオン API の採点機能について
アドオンでは、添付ファイルの成績の分子と分母の両方を設定できます。これらは、API の pointsEarned
値と maxPoints
値を使用してそれぞれ設定されます。Classroom UI の添付ファイル カードには、maxPoints
値が設定されている場合にその値が表示されます。
図 1. maxPoints
が設定された 3 つのアドオン添付ファイル カードを含む課題作成 UI。
Classroom アドオン API を使用すると、添付ファイルの成績の設定と獲得したスコアの設定を行うことができます。これらは課題の成績とは異なります。ただし、課題の成績設定は、添付ファイル カードに [成績の同期] ラベルが付いている添付ファイルの成績設定に従います。「成績の同期」の添付ファイルで生徒の提出物に pointsEarned
を設定すると、課題の生徒の下書きの成績も設定されます。
通常、maxPoints
を設定する課題に追加された最初の添付ファイルには、「成績の同期」ラベルが付けられます。図 1 に示す課題作成 UI の例で、[成績の同期] ラベルの例を確認してください。[Attachment 1](添付ファイル 1)カードに [Grade sync](成績の同期)ラベルが表示され、赤いボックス内の課題の成績が 50 点に更新されていることに注目してください。また、図 1 には 3 つの添付ファイル カードが表示されていますが、「Grade sync」というラベルが付いているのは 1 つのカードだけです。これは現在の実装の重要な制限事項です。「成績の同期」ラベルを付けられる添付ファイルは 1 つだけです。
maxPoints
が設定されている添付ファイルが複数ある場合、「成績の同期」が設定されている添付ファイルを削除しても、残りの添付ファイルで「成績の同期」が有効になることはありません。maxPoints
を設定する別の添付ファイルを追加すると、新しい添付ファイルで成績の同期が有効になり、割り当ての最大成績が調整されます。どの添付ファイルに「成績の同期」ラベルが付いているか、特定の課題に添付ファイルがいくつあるかをプログラムで確認するメカニズムはありません。
課題の最大評点を設定する
このセクションでは、課題の成績の分母(すべての生徒が提出物で獲得できる最大スコア)の設定について説明します。これを行うには、添付ファイルの maxPoints
値を設定します。
採点機能を有効にするために必要なのは、既存の実装に対するわずかな変更のみです。添付ファイルを作成するときに、studentWorkReviewUri
、teacherViewUri
、その他の添付ファイル フィールドを含む同じ AddOnAttachment
オブジェクトに maxPoints
値を追加します。
新しい課題のデフォルトの最大スコアは 100 です。グレードが正しく設定されていることを確認できるように、maxPoints
を 100 以外の値に設定することをおすすめします。デモとして maxPoints
を 50 に設定します。
Python
courses.courseWork.addOnAttachments
エンドポイントに CREATE
リクエストを発行する直前に、attachment
オブジェクトを作成するときに 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
値をローカルの Attachment データベースにも保存します。これにより、後で生徒の提出物を採点する際に、追加の 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 ではなく教師ビューの iframe が読み込まれるなど、予期しない動作が発生する可能性があります。したがって、pointsEarned
の設定方法には次の 2 つの選択肢があります。
- ログインしている教師の認証情報を使用します。
- 保存されている(オフラインの)教師用認証情報を使用する。
以降のセクションでは、各アプローチのトレードオフについて説明してから、各実装を示します。提供されている例では、成績を Classroom に渡す 2 つの方法の両方を示しています。提供されている例を実行する際にどちらの方法を選択するかについては、以下の言語固有の手順をご覧ください。
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 秒になることがあります。
これらの要因が組み合わさると、クラスの成績を完全に登録するために、教師がかなりの時間をかけて手作業を行う必要が生じる可能性があります。
このアプローチを実装するには、既存の StudentWorkReview ルートに 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 に成績が入力されるため、前述の問題を回避できることです。その結果、エンドユーザーは採点エクスペリエンスをシームレスで効率的だと認識します。また、この方法では、生徒がアクティビティを完了したときや非同期など、成績を返却するタイミングを選択できます。
このアプローチを実装するには、次のタスクを行います。
- アクセス トークンを保存するように User データベース レコードを変更します。
- 教師 ID を保存するように Attachment データベース レコードを変更します。
- 教師の認証情報を取得し、必要に応じて新しい Classroom サービス インスタンスを構築します。
- 提出物の成績を設定します。
このデモでは、生徒がアクティビティを完了したとき、つまり生徒ビューのルートで生徒がフォームを送信したときに、成績を設定します。
アクセス トークンを保存するように User データベース レコードを変更
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 を保存するように Attachment データベース レコードを変更
アクティビティの成績を設定するには、コースの教師として 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 に成績の下書きが表示されているはずです。課題を開いたときの生徒リストや、生徒の提出物の確認用 iframe の横にある [成績] ボックスにも表示されます。
- 教師が [生徒の提出物の確認] iframe を開いたときに成績を渡すように設定した場合、iframe の読み込みが完了するとすぐに [成績] ボックスに成績が表示されます。上記のとおり、この処理には 30 秒ほどかかることがあります。その後、特定の生徒の成績が Classroom の他の成績簿ビューにも表示されます。
生徒に正しいスコアが表示されていることを確認します。
これで、次のステップ(Google Classroom の外部で添付ファイルを作成する)に進む準備ができました。