這是 Classroom 外掛程式逐步操作說明系列中的第六逐步操作說明。
在本逐步操作說明中,您將修改先前逐步操作說明中的範例,產生「已評分」的活動類型附件。您也可以透過程式輔助方式將成績傳回 Google Classroom,這個成績會以暫定成績的形式顯示在老師的成績單中。
本逐步操作說明與其他系列文章中的其他人略有不同,其中提供將成績傳回 Classroom 的兩種可能方法。這兩者對開發人員和使用者體驗都不盡相同,請在設計 Classroom 外掛程式時同時考量。如要進一步瞭解實作選項,請參閱與附件互動指南頁面。
請注意,API 中的評分功能為「選用」性質。這些附件可以與任何活動類型附件搭配使用。
在本逐步操作說明中,您將完成下列操作:
- 修改先前對 Classroom API 的連結建立要求,以便一併設定附件的成績分母。
- 透過程式輔助方式為學生繳交的作業評分,並設定附件的成績分子。
- 實作兩種方法,使用已登入或離線的老師憑證,將繳交成績傳遞至 Classroom。
完成後,成績會在觸發回傳行為後顯示在 Classroom 成績單中。確切時機取決於實作方式。
為達此範例的目的,請重複使用先前逐步操作說明中的活動,其中學生會看到知名地標的圖片,並提示學生輸入該名稱。如果學生輸入正確的名稱,請為附件指派完整標記,否則為 0。
瞭解 Classroom 外掛程式 API 評分功能
外掛程式可以設定連結的成績分子和成績分母。這兩者是使用 API 中的 pointsEarned
和 maxPoints
值分別進行設定。Classroom UI 中的附件資訊卡會在已設定 maxPoints
值時顯示。
圖 1 在作業建立 UI 中顯示三個已設定 maxPoints
的外掛程式附件資訊卡。
Classroom 外掛程式 API 可讓您配置設定,以及設定針對「附件」成績獲得的分數。這與「指派」成績不同。不過,作業成績設定會遵循附件成績設定,該附件在附件資訊卡上有「成績同步處理」標籤。當「成績同步處理」附件為學生繳交的作業設定 pointsEarned
時,也會設定學生的作業草稿成績。
通常,在設定 maxPoints
的作業中新增的第一個附件會收到「成績同步處理」標籤。如需「成績同步處理」標籤的範例,請參閱圖 1 所示的作業建立 UI 範例。請注意,「附件 1」資訊卡上有「成績同步處理」標籤,紅色方塊中的作業成績已更新為 50 分。另請注意,雖然圖 1 顯示了三張附件資訊卡,但只有一張卡片含有「成績同步處理」標籤。這是目前實作的主要限制:只有一個連結可以有「成績同步處理」標籤。
如果有多個連結已設定 maxPoints
,移除設有「成績同步處理」的連結的連結不會在任何其餘連結上啟用「成績同步處理」功能。新增設定 maxPoints
的其他附件,即可在新附件上啟用 Grade Sync,且作業成績上限會隨之調整。目前還沒有機制能夠透過程式查看哪些附件具有「成績同步處理」標籤,也無法查看特定作業的附件數量。
設定附件的成績上限
本節說明如何設定附件成績的「分母」,也就是所有學生在提交後的可能最高分。方法是設定連結的 maxPoints
值。
您只需稍微修改現有的實作,即可啟用評分功能。建立附件時,請在包含 studentWorkReviewUri
、teacherViewUri
和其他附件欄位的同一個 AddOnAttachment
物件中新增 maxPoints
值。
請注意,新作業的預設分數上限是 100。建議您將 maxPoints
設為 100 以外的值,以便確認成績設定正確無誤。將 maxPoints
設為 50 做為示範:
Python
先新增 maxPoints
欄位給建構 attachment
物件,再於向 courses.courseWork.addOnAttachments
端點發出 CREATE
要求之前。依照我們提供的範例,您可以在 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
值。方法是在 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
的方法有兩種:
- 使用老師登入的憑證。
- 使用已儲存的 (離線) 老師憑證。
以下各節將探討各種方法的優缺點,再說明每個實作方式。請注意,我們提供的範例示範了將成績傳送到 Classroom 的「兩種」方法;請參閱下方的特定語言操作說明,瞭解如何在執行提供的範例時選取方法:
Python
找出 webapp/attachment_routes.py
檔案頂端的 SET_GRADE_WITH_LOGGED_IN_USER_CREDENTIALS
宣告。將這個值設為 True
,即可使用已登入的老師憑證傳回成績。將這個值設為 False
,即可在學生提交活動時,使用已儲存的憑證傳回成績。
使用老師登入的憑證設定成績
使用已登入使用者的憑證發出設定 pointsEarned
的要求。看起來應該相當符合直覺,因為它反映了到目前為止的實作內容,而且要稍微費力。
不過,請看老師「只會」在學生作業回顧 iframe 中與學生的提交內容互動。這對您來說有重大影響:
- 老師必須先在 Classroom UI 中採取行動,才能在 Classroom 中填入成績。
- 老師可能需要開啟所有學生繳交的作業,才能填入所有學生的成績。
- Classroom 收到成績後,會在 Classroom UI 中顯示短暫延遲。延遲時間通常為五至十秒,但也可能長達 30 秒。
如果組合這些因素,老師可能需要花費大量時間的人工作業來完整填入課程的成績。
如要實作這個方法,請在現有的學生工作審查路徑中另外新增一個 API 呼叫。
擷取學生提交與附件記錄後,請評估學生提交的內容並儲存結果。請在 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()
使用老師的離線憑證設定成績
第二種設定成績的方法,需要為建立該連結的老師使用已儲存的憑證。如要進行這項實作,您必須使用先前授權的老師提供的更新和存取權杖來建構憑證,然後使用這些憑證設定 pointsEarned
。
這種做法的一大優勢,是可在 Classroom UI 中直接填入成績,不必讓老師採取行動,避免上述問題。這樣一來,使用者就會認為評分體驗流暢又有效率。此外,這個方法可讓您選擇要傳回成績的時間點,例如學生完成活動或以非同步方式完成活動。
如要實作這個方法,請完成下列工作:
- 修改使用者資料庫記錄,以儲存存取權杖。
- 修改附件資料庫記錄以儲存老師 ID。
- 擷取老師的憑證,並視需要建構新的 Classroom 服務執行個體。
- 設定繳交作業的成績。
為進行示範,請在學生完成活動時設定成績,也就是學生透過「學生檢視畫面」路徑提交表單時。
修改使用者資料庫記錄以儲存存取權杖
需要兩個不重複的符記才能發出 API 呼叫:「更新權杖」和「存取權杖」。如果您到目前為止都是按照逐步說明系列操作,則 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 的路徑。將學生的回應儲存至本機資料庫後,系統會立即從本機儲存空間擷取老師的憑證。鑒於前兩個步驟已做好準備,因此應該相當簡單。您也可以使用這些通知,為老師使用者建構新的 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 外建立附件。