附件成績和成績回傳式曝光

這是 Classroom 外掛程式逐步操作說明系列的第六篇。

在本逐步操作教學課程中,您將修改上一個逐步操作教學課程步驟中的範例,產生「已評分」活動類型附件。您也可以透過程式輔助將成績傳回 Google Classroom,這會顯示在老師的成績單中,做為草稿成績。

本逐步解說與本系列的其他逐步解說略有不同,因為有兩種可能的方法可將成績傳回 Classroom。這兩者對開發人員和使用者體驗都有不同影響,設計 Classroom 外掛程式時請一併考量。如要進一步瞭解實作選項,請參閱「與附件互動指南頁面」。

請注意,API 中的評分功能為選用功能。這些動作可用於任何活動類型附件

在本逐步解說中,您將完成下列操作:

  • 修改先前的附件建立要求,向 Classroom API 一併設定附件的成績分母。
  • 以程式輔助方式為學生提交的作業評分,並設定附件的成績分子。
  • 使用已登入或離線的老師憑證,實作兩種方法,將提交內容的分數傳送至 Classroom。

完成後,系統會觸發回傳行為,成績就會顯示在 Classroom 成績記錄中。確切時間取決於導入方式。

在本範例中,請重複使用先前逐步解說中的活動,讓學生看到著名地標的圖片,並輸入地標名稱。如果學生輸入的名稱正確,請給予附件滿分,否則給予零分。

瞭解 Classroom 外掛程式 API 的評分功能

外掛程式可以為附件設定成績分子和分母。這些值分別是使用 API 中的 pointsEarnedmaxPoints 值設定。如果已設定 maxPoints 值,Classroom UI 中的附件卡片就會顯示該值。

一個作業有多個附件,且其中一個附件有 maxPoints 的範例

圖 1. 作業建立 UI,其中有三張外掛程式附件卡片,且已設定 maxPoints

Classroom 外掛程式 API 可讓您設定附件的成績,並設定獲得的分數。這與作業成績不同,不過,作業成績設定會採用附件卡片上標有「成績同步」標籤的附件成績設定。當「成績同步」附件為學生提交的作業設定 pointsEarned 時,也會設定該作業的學生草稿成績。

通常,作業中第一個設有分數的附件會顯示「成績同步處理」標籤。maxPoints如要查看「Grade sync」標籤的範例,請參閱圖 1 所示的作業建立 UI 範例。請注意,「附件 1」卡片有「成績同步」標籤,且紅框中的作業成績已更新為 50 分。另請注意,雖然圖 1 顯示三張附件資訊卡,但只有一張資訊卡有「成績同步」標籤。這是目前實作方式的主要限制:只有一個附件可以有「成績同步」標籤

如果有多個附件已設定 maxPoints,移除「成績同步」不會在任何其餘附件上啟用「成績同步」。新增另一個附件並設定 maxPoints 會在新附件上啟用成績同步功能,且作業最高成績會隨之調整。您無法以程式輔助方式查看哪個附件有「成績同步」標籤,也無法查看特定作業有多少附件。

設定附件的最高分數

本節說明如何設定附件成績的分母,也就是所有學生提交作業後可獲得的最高分數。方法是設定附件的 maxPoints 值。

啟用評分功能時,只需要對現有實作項目進行小幅修改。建立附件時,請在包含 studentWorkReviewUriteacherViewUri 和其他附件欄位的相同 AddOnAttachment 物件中,新增 maxPoints 值。

請注意,新指派項目的預設最高分數為 100 分。建議您將 maxPoints 設為 100 以外的值,確認成績設定正確無誤。將 maxPoints 設為 50 做為示範:

Python

建構 attachment 物件時,請新增 maxPoints 欄位,然後再向 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 值儲存在本機 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

  • 使用登入老師的憑證。
  • 使用儲存的 (離線) 老師憑證。

以下各節將討論每種做法的取捨考量,然後示範各項實作方式。請注意,我們提供的範例會示範兩種將成績傳送至 Classroom 的方法;請參閱下方的語言專屬操作說明,瞭解如何執行提供的範例時選取方法:

Python

webapp/attachment_routes.py 檔案頂端找出 SET_GRADE_WITH_LOGGED_IN_USER_CREDENTIALS 宣告。將這個值設為 True,即可使用登入老師的憑證傳回成績。將這個值設為 False,即可在學生提交活動時,使用儲存的憑證傳回成績。

使用登入老師的憑證設定成績

使用已登入使用者的憑證,發出要求來設定 pointsEarned。這應該相當直覺,因為它反映了目前為止的其餘實作項目,而且只需稍加努力即可實現。

不過請注意,老師只能在「學生作業審查」iframe 中與學生的作業互動。這會產生一些重要影響:

  • 老師必須在 Classroom 使用者介面中採取行動,系統才會在 Classroom 中填入成績。
  • 老師可能必須開啟每位學生的提交內容,才能填入所有學生的成績。
  • Classroom 收到成績後,需要過一段時間才會顯示在 Classroom 使用者介面中。延遲時間通常為 5 到 10 秒,但最長可達 30 秒。

綜合上述因素,老師可能需要花費大量時間手動作業,才能完整填寫課程的成績。

如要實作這個方法,請在現有的 StudentWork Review 路線中新增一個 API 呼叫。

擷取學生提交的作業和附件記錄後,評估學生提交的作業並儲存所得成績。在 AddOnAttachmentStudentSubmission 物件pointsEarned 欄位中設定等級。最後,向 courses.courseWork.addOnAttachments.studentSubmissions 端點發出 PATCH 要求,並在要求主體中加入 AddOnAttachmentStudentSubmission 例項。請注意,我們也需要在 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 使用者介面中採取任何動作,因此可避免上述問題。因此,使用者會覺得評分體驗流暢有效率。此外,您也可以選擇發還成績的時間,例如在學生完成活動時或非同步發還。

如要採用這種做法,請完成下列工作:

  1. 修改使用者資料庫記錄,儲存存取權杖。
  2. 修改附件資料庫記錄,儲存老師 ID。
  3. 擷取老師的憑證,並視需要建構新的 Classroom 服務執行個體。
  4. 設定提交內容的成績。

為進行這項示範,請在學生完成活動時設定成績,也就是學生透過「學生檢視」路徑提交表單時。

修改使用者資料庫記錄,儲存存取權杖

如要進行 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,
    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 以外建立附件