ルーブリックを使ってみる

rubric は、教師が生徒の提出物を採点する際に使用できるテンプレートです。Classroom API を使用すると、教師に代わってこれらのルーブリックを管理したり、生徒の提出物のルーブリックの成績を読み取ったりできます。

Classroom の UI でのルーブリックの表示 図 1.Classroom の課題のルーブリックのサンプル。

このガイドでは、ルーブリック API の基本的なコンセプトと機能について説明します。 ルーブリックの一般的な構造と、Classroom UI でルーブリックを使用して採点する方法については、こちらのヘルプセンター記事をご覧ください。

前提条件

このガイドでは、次のものがあることを前提としています。

デスクトップ アプリケーションの認証情報を認可する

エンドユーザーとして認証を行い、アプリ内でユーザーデータにアクセスするには、1 つ以上の OAuth 2.0 クライアント ID を作成する必要があります。クライアント ID は、Google の OAuth サーバーで個々のアプリを識別するために使用します。アプリが複数のプラットフォームで実行される場合は、プラットフォームごとに個別のクライアント ID を作成する必要があります。

  1. Google Cloud コンソールの Google Cloud の [認証情報] ページに 移動します。
  2. [認証情報を作成] > [OAuth クライアント ID] をクリックします。
  3. [アプリケーションの種類] > [デスクトップ アプリ] をクリックします。
  4. [名前] フィールドに、認証情報の名前を入力します。この名前は Google Cloud コンソールにのみ表示されます。例: 「ルーブリック クライアント」。
  5. [作成] をクリックします。[OAuth クライアントを作成しました] 画面が表示され、新しいクライアント ID とクライアント シークレットが表示されます。
  6. [Download JSON] をクリックし、[OK] をクリックします。新しく作成した認証情報が [OAuth 2.0 クライアント ID] に表示されます。
  7. ダウンロードした JSON ファイルを credentials.json として保存し、作業ディレクトリに移動します。
  8. [認証情報を作成] > [API キー] をクリックし、API キーをメモします。

詳細については、アクセス認証情報を作成するをご覧ください。

OAuth スコープを構成する

プロジェクトの既存の OAuth スコープによっては、追加のスコープを構成する必要があります。

  1. OAuth 同意画面に移動します。
  2. [アプリを編集] > [保存して次へ] をクリックして、[スコープ] 画面に移動します。
  3. [スコープを追加または削除] をクリックします。
  4. まだ追加していない場合は、次のスコープを追加します。
    • https://www.googleapis.com/auth/classroom.coursework.students
    • https://www.googleapis.com/auth/classroom.courses
  5. 次に、[更新] > [保存して次へ] > [保存して次へ] > [ダッシュボードに戻る] をクリックします。

詳細については、OAuth 同意画面を構成するをご覧ください。

classroom.coursework.students スコープを使用すると、ルーブリック(CourseWork へのアクセスを含む)に対する読み取りと書き込みのアクセスが可能になります。classroom.courses スコープを使用すると、コースの読み取りと書き込みが可能になります。

特定のメソッドに必要なスコープは、そのメソッドのリファレンス ドキュメントに記載されています。例として、courses.courseWork.rubrics.create 認可スコープ をご覧ください。Classroom のすべてのスコープは、Google API の OAuth 2.0 スコープ で確認できます

サンプルを構成する

作業ディレクトリで、Python 用 Google クライアント ライブラリをインストールします。

pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
でプレビュー版 API を使用する方法については、プレビュー版 API にアクセスする をご覧ください。

クライアント ライブラリをビルドしてユーザーを認可する main.py というファイルを作成します。YOUR_API_KEY の代わりに API キーを使用します。

import json
import os.path

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/classroom.courses',
          'https://www.googleapis.com/auth/classroom.coursework.students']

def build_authenticated_service(api_key):
    """Builds the Classroom service."""
    creds = None
    # The file token.json stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run.
        with open('token.json', 'w') as token:
            token.write(creds.to_json())

    try:
        # Build the Classroom service.
        service = build(
            serviceName="classroom",
            version="v1",
            credentials=creds,
            discoveryServiceUrl=f"https://classroom.googleapis.com/$discovery/rest?labels=DEVELOPER_PREVIEW&key={api_key}")

        return service

    except HttpError as error:
        print('An error occurred: %s' % error)

if __name__ == '__main__':
    service = build_authenticated_service(YOUR_API_KEY)

python main.py を使用してスクリプトを実行します。ログインして OAuth スコープに同意するよう求められます。

課題を作成する

ルーブリックは課題(CourseWork)に関連付けられており、その CourseWork のコンテキストでのみ意味を持ちます。ルーブリックを作成できるのは 親の CourseWork アイテムを作成した Google Cloud プロジェクトのみです。このガイドでは、スクリプトを使用して新しい CourseWork 課題を作成します。

main.py に以下を追加します。

def get_latest_course(service):
    """Retrieves the last created course."""
    try:
        response = service.courses().list(pageSize=1).execute()
        courses = response.get("courses", [])
        if not courses:
            print("No courses found. Did you remember to create one in the UI?")
            return
        course = courses[0]
        return course

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

def create_coursework(service, course_id):
    """Creates and returns a sample coursework."""
    try:
        coursework = {
            "title": "Romeo and Juliet analysis.",
            "description": """Write a paper arguing that Romeo and Juliet were
                                time travelers from the future.""",
            "workType": "ASSIGNMENT",
            "state": "PUBLISHED",
        }
        coursework = service.courses().courseWork().create(
            courseId=course_id, body=coursework).execute()
        return coursework

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

次に、main.py を更新して、作成したテストクラスの course_id を取得し、新しいサンプル課題を作成して、課題の coursework_id を取得します。

if __name__ == '__main__':
    service = build_authenticated_service(YOUR_API_KEY)

    course = get_latest_course(service)
    course_id = course.get("id")
    course_name = course.get("name")
    print(f"'{course_name}' course ID: {course_id}")

    coursework = create_coursework(service, course_id)
    coursework_id = coursework.get("id")
    print(f"Assignment created with ID {coursework_id}")

    #TODO(developer): Save the printed course and coursework IDs.

course_idcoursework_id を保存します。これらは、すべてのルーブリック CRUD オペレーションに必要です。

これで、Classroom にサンプル CourseWork が作成されました。

Classroom UI での課題の表示 図 2.Classroom の課題のサンプル。

ユーザーの資格を確認する

ルーブリックを作成して更新するには、リクエストを行うユーザーと対応するコースのオーナーの両方に Google Workspace for Education Plusライセンスが割り当てられている必要があります。 Classroom は、ユーザーがアクセスできる機能をデベロッパーが判断できるように、ユーザーの 資格エンドポイントをサポートしています。

main.py を更新して実行し、テストアカウントがルーブリック機能にアクセスできることを確認します。

if __name__ == '__main__':
    service = build_authenticated_service(YOUR_API_KEY)

    capability = service.userProfiles().checkUserCapability(
        userId='me',
        # Specify the preview version. checkUserCapability is
        # supported in V1_20240930_PREVIEW and later.
        previewVersion="V1_20240930_PREVIEW",
        capability="CREATE_RUBRIC").execute()

    if not capability.get('allowed'):
      print('User ineligible for rubrics creation.')
      # TODO(developer): in a production app, this signal could be used to
      # proactively hide any rubrics related features from users or encourage
      # them to upgrade to the appropriate license.
    else:
      print('User eligible for rubrics creation.')

ルーブリックの作成

これで、ルーブリックの管理を開始する準備が整いました。

ルーブリックは、完全なルーブリック オブジェクトを含む CourseWork を使用して create() 呼び出しで作成できます。条件とレベルの ID プロパティは 省略されます(これらは作成時に生成されます)。

次の関数を main.py に追加します。

def create_rubric(service, course_id, coursework_id):
    """Creates an example rubric on a coursework."""
    try:
        body = {
            "criteria": [
                {
                    "title": "Argument",
                    "description": "How well structured your argument is.",
                    "levels": [
                        {"title": "Convincing",
                         "description": "A compelling case is made.", "points": 30},
                        {"title": "Passable",
                         "description": "Missing some evidence.", "points": 20},
                        {"title": "Needs Work",
                         "description": "Not enough strong evidence..", "points": 0},
                    ]
                },
                {
                    "title": "Spelling",
                    "description": "How well you spelled all the words.",
                    "levels": [
                        {"title": "Perfect",
                         "description": "No mistakes.", "points": 20},
                        {"title": "Great",
                         "description": "A mistake or two.", "points": 15},
                        {"title": "Needs Work",
                         "description": "Many mistakes.", "points": 5},
                    ]
                },
                {
                    "title": "Grammar",
                    "description": "How grammatically correct your sentences are.",
                    "levels": [
                        {"title": "Perfect",
                         "description": "No mistakes.", "points": 20},
                        {"title": "Great",
                         "description": "A mistake or two.", "points": 15},
                        {"title": "Needs Work",
                         "description": "Many mistakes.", "points": 5},
                    ]
                },
            ]
        }

        rubric = service.courses().courseWork().rubrics().create(
            courseId=course_id, courseWorkId=coursework_id, body=body
            ).execute()
        print(f"Rubric created with ID {rubric.get('id')}")
        return rubric

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

次に、main.py を更新して実行し、以前の CourseCourseWork ID を使用してルーブリックの例を作成します。

if __name__ == '__main__':
    service = build_authenticated_service(YOUR_API_KEY)

    capability = service.userProfiles().checkUserCapability(
        userId='me',
        # Specify the preview version. checkUserCapability is
        # supported in V1_20240930_PREVIEW and later.
        previewVersion="V1_20240930_PREVIEW",
        capability="CREATE_RUBRIC").execute()

    if not capability.get('allowed'):
      print('User ineligible for rubrics creation.')
      # TODO(developer): in a production app, this signal could be used to
      # proactively hide any rubrics related features from users or encourage
      # them to upgrade to the appropriate license.
    else:
      rubric = create_rubric(service, YOUR_COURSE_ID, YOUR_COURSEWORK_ID)
      print(json.dumps(rubric, indent=4))

ルーブリックの表現に関する注意点:

  • 条件とレベルの順序は Classroom UI に反映されます。
  • スコア付きレベル(points プロパティを持つレベル)は、ポイントで昇順または降順に並べ替える必要があります(ランダムに並べ替えることはできません)。
  • 教師は UI で条件とスコア付きレベル(スコアなしレベルは除く)を並べ替えることができ、データ内の順序が変更されます。

ルーブリックの構造に関するその他の注意事項については、制限事項をご覧ください。

UI に戻ると、課題にルーブリックが表示されます。

Classroom の UI でのルーブリックの表示 図 3.Classroom の課題のルーブリックのサンプル。

ルーブリックを読み取る

ルーブリックは、標準の list()get() メソッドで読み取ることができます。

課題にルーブリックは 1 つしか存在できないため、list() は直感的ではないように思えますが、ルーブリック ID がまだない場合に便利です。CourseWork に関連付けられたルーブリックがない場合、list() レスポンスは空になります。

次の関数を main.py に追加します。

def get_rubric(service, course_id, coursework_id):
    """
    Get the rubric on a coursework. There can only be at most one.
    Returns null if there is no rubric.
    """
    try:
        response = service.courses().courseWork().rubrics().list(
            courseId=course_id, courseWorkId=coursework_id
            ).execute()

        rubrics = response.get("rubrics", [])
        if not rubrics:
            print("No rubric found for this assignment.")
            return
        rubric = rubrics[0]
        return rubric

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

main.py を更新して実行し、追加したルーブリックを取得します。

if __name__ == '__main__':
    service = build_authenticated_service(YOUR_API_KEY)

    rubric = get_rubric(service, YOUR_COURSE_ID, YOUR_COURSEWORK_ID)
    print(json.dumps(rubric, indent=4))

    #TODO(developer): Save the printed rubric ID.

後のステップで使用するため、ルーブリックの id プロパティをメモします。

ルーブリック ID がある場合は、Get() が適しています。関数の代わりに get() を使用すると、次のようになります。

def get_rubric(service, course_id, coursework_id, rubric_id):
    """
    Get the rubric on a coursework. There can only be at most one.
    Returns a 404 if there is no rubric.
    """
    try:
        rubric = service.courses().courseWork().rubrics().get(
            courseId=course_id,
            courseWorkId=coursework_id,
            id=rubric_id
        ).execute()

        return rubric

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

ルーブリックがない場合、この実装は 404 を返します。

ルーブリックを更新する

ルーブリックの更新は patch() 呼び出しで行います。ルーブリックの構造が複雑なため、更新は読み取り、変更、書き込みのパターンで行う必要があります。このパターンでは、criteria プロパティ全体が置き換えられます。

更新ルールは次のとおりです。

  1. ID なしで追加 された条件またはレベルは、追加 とみなされます。
  2. 以前に存在していた条件またはレベルが欠落 している場合は、削除 とみなされます。
  3. 既存の ID があり、データが変更されている 条件またはレベルは、編集 とみなされます。変更されていないプロパティはそのまま残ります。
  4. 新しい ID または不明な ID が指定された条件またはレベルは、エラー とみなされます。
  5. 新しい条件とレベルの順序は、新しい UI の順序とみなされます (前述の制限があります)。

ルーブリックを更新する関数を追加します。

def update_rubric(service, course_id, coursework_id, rubric_id, body):
    """
    Updates the rubric on a coursework.
    """
    try:
        rubric = service.courses().courseWork().rubrics().patch(
            courseId=course_id,
            courseWorkId=coursework_id,
            id=rubric_id,
            body=body,
            updateMask='criteria'
        ).execute()

        return rubric

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

この例では、criteria フィールドを、 updateMask を使用して、変更するフィールドとして指定しています。

次に、前述の更新ルールごとに変更を行うように main.py を変更します。

if __name__ == '__main__':
    service = build_authenticated_service(YOUR_API_KEY)

    capability = service.userProfiles().checkUserCapability(
        userId='me',
        # Specify the preview version. checkUserCapability is
        # supported in V1_20240930_PREVIEW and later.
        previewVersion="V1_20240930_PREVIEW",
        capability="CREATE_RUBRIC").execute()

    if not capability.get('allowed'):
      print('User ineligible for rubrics creation.')
      # TODO(developer): in a production app, this signal could be used to
      # proactively hide any rubrics related features from users or encourage
      # them to upgrade to the appropriate license.
    else:
        # Get the latest rubric.
        rubric = get_rubric(service, YOUR_COURSE_ID, YOUR_COURSEWORK_ID)
        criteria = rubric.get("criteria")
        """
        The "criteria" property should look like this:
        [
            {
                "id": "NkEyMdMyMzM2Nxkw",
                "title": "Argument",
                "description": "How well structured your argument is.",
                "levels": [
                    {
                        "id": "NkEyMdMyMzM2Nxkx",
                        "title": "Convincing",
                        "description": "A compelling case is made.",
                        "points": 30
                    },
                    {
                        "id": "NkEyMdMyMzM2Nxky",
                        "title": "Passable",
                        "description": "Missing some evidence.",
                        "points": 20
                    },
                    {
                        "id": "NkEyMdMyMzM2Nxkz",
                        "title": "Needs Work",
                        "description": "Not enough strong evidence..",
                        "points": 0
                    }
                ]
            },
            {
                "id": "NkEyMdMyMzM2Nxk0",
                "title": "Spelling",
                "description": "How well you spelled all the words.",
                "levels": [...]
            },
            {
                "id": "NkEyMdMyMzM2Nxk4",
                "title": "Grammar",
                "description": "How grammatically correct your sentences are.",
                "levels": [...]
            }
        ]
        """

        # Make edits. This example will make one of each type of change.

        # Add a new level to the first criteria. Levels must remain sorted by
        # points.
        new_level = {
            "title": "Profound",
            "description": "Truly unique insight.",
            "points": 50
        }
        criteria[0]["levels"].insert(0, new_level)

        # Remove the last criteria.
        del criteria[-1]

        # Update the criteria titles with numeric prefixes.
        for index, criterion in enumerate(criteria):
            criterion["title"] = f"{index}: {criterion['title']}"

        # Resort the levels from descending to ascending points.
        for criterion in criteria:
            criterion["levels"].sort(key=lambda level: level["points"])

        # Update the rubric with a patch call.
        new_rubric = update_rubric(
            service, YOUR_COURSE_ID, YOUR_COURSEWORK_ID, YOUR_RUBRIC_ID, rubric)

        print(json.dumps(new_rubric, indent=4))

変更は Classroom の教師に反映されます。

Classroom の UI で更新されたルーブリックを表示した様子 図 4.更新されたルーブリック。

ルーブリックで採点された提出物を表示する

現時点では、API でルーブリックを使用して生徒の提出物を採点することはできませんが、Classroom UI でルーブリックを使用して採点された提出物のルーブリックの成績を読み取ることができます。

Classroom UI で生徒として、サンプル課題を完了して提出します。 次に、教師として、ルーブリックを使用して課題を手動で採点します

Classroom の UI でのルーブリックの成績の表示 図 5.採点中のルーブリックの教師ビュー。

StudentSubmissions がルーブリックで採点された場合、2 つの新しい プロパティがあります。draftRubricGradesassignedRubricGrades です。これらは、下書きと割り当てられた採点 状態のときに教師が選択した ポイントとレベルを表します。

既存の studentSubmissions.get()studentSubmissions.list() メソッドを使用して、採点済みの提出物を表示できます。

次の関数を main.py に追加して、生徒の提出物を一覧表示します。

def get_latest_submission(service, course_id, coursework_id):
    """Retrieves the last submission for an assignment."""
    try:
        response = service.courses().courseWork().studentSubmissions().list(
            courseId = course_id,
            courseWorkId = coursework_id,
            pageSize=1
        ).execute()
        submissions = response.get("studentSubmissions", [])
        if not submissions:
            print(
                """No submissions found. Did you remember to turn in and grade
                   the assignment in the UI?""")
            return
        submission = submissions[0]
        return submission

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

次に、main.py を更新して実行し、提出物の成績を表示します。

if __name__ == '__main__':
    service = build_authenticated_service(YOUR_API_KEY)

    submission = get_latest_submission(
        service, YOUR_COURSE_ID, YOUR_COURSEWORK_ID)
    print(json.dumps(submission, indent=4))

draftRubricGradesassignedRubricGrades には次のものが含まれます。

  • 対応するルーブリック条件の criterionId
  • 教師が各条件に割り当てた points。これは選択したレベルから取得できますが、教師がオーバーライドすることもできます。
  • 各条件に選択したレベルの levelId。教師がレベルを選択しなかったが、条件にポイントを割り当てた場合、このフィールドは存在しません。

これらのリストには、教師がレベルを選択したか、ポイントを設定した条件のエントリのみが含まれます。たとえば、教師が採点中に 1 つの条件のみを操作する場合、ルーブリックに多くの条件があっても、draftRubricGradesassignedRubricGrades には 1 つのアイテムしか含まれません。

ルーブリックを削除する

ルーブリックは、標準の delete() リクエストで削除できます。次のコードは、完全な関数例を示していますが、採点がすでに開始されているため、現在のルーブリックを削除することはできません。

def delete_rubric(service, course_id, coursework_id, rubric_id):
    """Deletes the rubric on a coursework."""
    try:
        service.courses().courseWork().rubrics().delete(
            courseId=course_id,
            courseWorkId=coursework_id,
            id=rubric_id
        ).execute()

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

ルーブリックをエクスポートしてインポートする

ルーブリックは、教師が再利用できるように Google スプレッドシートに手動でエクスポートできます。

コードでルーブリック条件を指定するだけでなく、criteria の代わりにルーブリック本文に sourceSpreadsheetId を指定することで、エクスポートしたシートからルーブリックを作成して更新することもできます。

def create_rubric_from_sheet(service, course_id, coursework_id, sheet_id):
    """Creates an example rubric on a coursework."""
    try:
        body = {
            "sourceSpreadsheetId": sheet_id
        }

        rubric = service.courses().courseWork().rubrics().create(
            courseId=course_id, courseWorkId=coursework_id, body=body
            ).execute()

        print(f"Rubric created with ID {rubric.get('id')}")
        return rubric

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error