콘텐츠 형식 첨부파일

이 둘러보기는 클래스룸 부가기능 둘러보기 시리즈의 네 번째 둘러보기입니다.

이 둘러보기에서는 Google 클래스룸 API와 상호작용하여 첨부파일을 만듭니다. 사용자가 첨부파일 콘텐츠를 볼 수 있는 경로를 제공합니다. 뷰는 수업에서 사용자의 역할에 따라 다릅니다. 이 둘러보기에서는 콘텐츠 유형 첨부파일을 다룹니다. 학생 제출물이 필요하지 않습니다.

이 둘러보기 과정에서 다음을 완료합니다.

  • 다음 부가기능 쿼리 매개변수를 검색하고 사용합니다.
    • addOnToken: 첨부파일 검색 뷰에 전달되는 승인 토큰입니다.
    • itemId: 부가기능 첨부파일을 수신하는 CourseWork, CourseWorkMaterial 또는 공지사항의 고유 식별자입니다.
    • itemType: 'courseWork', 'courseWorkMaterials' 또는 'announcement'입니다.
    • courseId: 과제가 생성되는 Google 클래스룸 수업의 고유 식별자입니다.
    • attachmentId: 생성 후 Google 클래스룸에서 부가기능 첨부파일에 할당하는 고유 식별자입니다.
  • 콘텐츠 유형 첨부파일의 영구 스토리지를 구현합니다.
  • 첨부파일을 만들고 교사 뷰 및 학생 뷰 iframe을 제공하는 경로를 제공합니다.
  • Google 클래스룸 부가기능 API에 다음 요청을 실행합니다.
    • 새 첨부파일을 만듭니다.
    • 로그인한 사용자가 학생인지 교사인지 식별하는 부가기능 컨텍스트를 가져옵니다.

완료되면 교사로 로그인했을 때 Google 클래스룸 UI를 통해 과제에 콘텐츠 유형 첨부파일을 만들 수 있습니다. 수업의 교사와 학생도 콘텐츠를 볼 수 있습니다.

클래스룸 API 사용 설정

이 단계부터 클래스룸 API를 호출합니다. API를 호출하려면 먼저 Google Cloud 프로젝트에서 API를 사용 설정해야 합니다. Google 클래스룸 API 라이브러리 항목으로 이동하여 사용 설정을 선택합니다.

첨부파일 검색 뷰 쿼리 매개변수 처리

앞서 설명한 대로 Google 클래스룸은 iframe에서 첨부파일 검색 뷰를 로드할 때 쿼리 매개변수를 전달합니다.

  • courseId: 현재 클래스룸 수업의 ID입니다.
  • itemId: 부가기능 첨부파일을 수신하는 CourseWork, CourseWorkMaterial 또는 공지사항의 고유 식별자입니다.
  • itemType: 'courseWork', 'courseWorkMaterials' 또는 'announcement'입니다.
  • addOnToken: 특정 클래스룸 부가기능 작업을 승인하는 데 사용되는 토큰입니다.
  • login_hint: 현재 사용자의 Google ID입니다.

이 둘러보기에서는 courseId, itemId, itemType, addOnToken을 다룹니다. 클래스룸 API를 호출할 때 이러한 매개변수를 유지하고 전달합니다.

이전 둘러보기 단계와 마찬가지로 전달된 쿼리 매개변수 값을 세션에 저장합니다. 첨부파일 검색 뷰가 처음 열릴 때 이렇게 하는 것이 중요합니다. 클래스룸에서 이러한 쿼리 매개변수를 전달할 수 있는 유일한 기회이기 때문입니다.

Python

첨부파일 검색 뷰의 경로를 제공하는 Flask 서버 파일로 이동합니다 (제공된 예를 따르는 경우 attachment-discovery-routes.py). 부가기능 방문 페이지 경로의 상단(제공된 예의 /classroom-addon)에서 courseId, itemId, itemType, addOnToken 쿼리 매개변수를 검색하고 저장합니다.

# Retrieve the itemId, courseId, and addOnToken query parameters.
if flask.request.args.get("itemId"):
    flask.session["itemId"] = flask.request.args.get("itemId")
if flask.request.args.get("itemType"):
    flask.session["itemType"] = flask.request.args.get("itemType")
if flask.request.args.get("courseId"):
    flask.session["courseId"] = flask.request.args.get("courseId")
if flask.request.args.get("addOnToken"):
    flask.session["addOnToken"] = flask.request.args.get("addOnToken")

이러한 값은 있는 경우에만 세션에 작성합니다. 사용자가 나중에 iframe을 닫지 않고 첨부파일 검색 뷰로 돌아가는 경우 다시 전달되지 않습니다.

콘텐츠 유형 첨부파일의 영구 스토리지 추가

생성된 첨부파일의 로컬 레코드가 필요합니다. 이렇게 하면 교사가 클래스룸에서 제공한 식별자를 사용하여 선택한 콘텐츠를 조회할 수 있습니다.

Attachment의 데이터베이스 스키마를 설정합니다. 제공된 예에서는 이미지와 캡션을 보여주는 첨부파일을 제공합니다. Attachment에는 다음과 같은 속성이 포함되어 있습니다.

  • attachment_id: 첨부파일의 고유 식별자입니다. 클래스룸에서 할당하고 첨부파일을 만들 때 응답으로 반환됩니다.
  • image_filename: 표시할 이미지의 로컬 파일 이름입니다.
  • image_caption: 이미지와 함께 표시할 캡션입니다.

Python

이전 단계에서 SQLite 및 flask_sqlalchemy 구현을 확장합니다.

사용자 테이블을 정의한 파일로 이동합니다 (제공된 예를 따르는 경우 models.py). User 클래스 아래 파일 하단에 다음을 추가합니다.

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))

새 Attachment 클래스를 첨부파일 처리 경로가 있는 서버 파일로 가져옵니다.

새 경로 설정

애플리케이션에서 새 페이지를 설정하여 이 둘러보기 단계를 시작합니다. 이렇게 하면 사용자가 부가기능을 통해 콘텐츠를 만들고 볼 수 있습니다.

첨부파일 생성 경로 추가

교사가 콘텐츠를 선택하고 첨부파일 생성 요청을 실행할 수 있는 페이지가 필요합니다. /attachment-options 경로를 구현하여 교사가 선택할 수 있는 콘텐츠 옵션을 표시합니다. 콘텐츠 선택 및 생성 확인 페이지의 템플릿도 필요합니다. 제공된 예에는 이러한 템플릿이 포함되어 있으며 클래스룸 API의 요청 및 응답도 표시할 수 있습니다.

/attachment-options 페이지를 만드는 대신 기존 첨부파일 검색 뷰 방문 페이지를 수정하여 콘텐츠 옵션을 표시할 수도 있습니다. 이 연습에서는 새 페이지를 만드는 것이 좋습니다. 이렇게 하면 앱 권한 취소와 같이 두 번째 둘러보기 단계에서 구현된 SSO 동작을 유지할 수 있습니다. 부가기능을 빌드하고 테스트할 때 유용합니다.

교사는 제공된 예에서 캡션이 있는 이미지의 작은 집합 중에서 선택할 수 있습니다. 파일 이름에서 파생된 캡션이 있는 유명한 랜드마크의 이미지 4개를 제공했습니다.

Python

제공된 예에서는 webapp/attachment_routes.py 파일에 있습니다.

@app.route("/attachment-options", methods=["GET", "POST"])
def attachment_options():
    """
    Render the attachment options page from the "attachment-options.html"
    template.

    This page displays a grid of images that the user can select using
    checkboxes.
    """

    # A list of the filenames in the static/images directory.
    image_filenames = os.listdir(os.path.join(app.static_folder, "images"))

    # The image_list_form_builder method creates a form that displays a grid
    # of images, checkboxes, and captions with a Submit button. All images
    # passed in image_filenames will be shown, and the captions will be the
    # title-cased filenames.

    # The form must be built dynamically due to limitations in WTForms. The
    # image_list_form_builder method therefore also returns a list of
    # attribute names in the form, which will be used by the HTML template
    # to properly render the form.
    form, var_names = image_list_form_builder(image_filenames)

    # If the form was submitted, validate the input and create the attachments.
    if form.validate_on_submit():

        # Build a dictionary that maps image filenames to captions.
        # There will be one dictionary entry per selected item in the form.
        filename_caption_pairs = construct_filename_caption_dictionary_list(
            form)

        # Check that the user selected at least one image, then proceed to
        # make requests to the Classroom API.
        if len(filename_caption_pairs) > 0:
            return create_attachments(filename_caption_pairs)
        else:
            return flask.render_template(
                "create-attachment.html",
                message="You didn't select any images.",
                form=form,
                var_names=var_names)

    return flask.render_template(
        "attachment-options.html",
        message=("You've reached the attachment options page. "
                "Select one or more images and click 'Create Attachment'."),
        form=form,
        var_names=var_names,
    )

그러면 다음과 같은 '첨부파일 만들기' 페이지가 생성됩니다.

Python 예시 콘텐츠 선택 뷰

교사는 여러 이미지를 선택할 수 있습니다. 교사가 create_attachments 메서드에서 선택한 각 이미지에 대해 하나의 첨부파일을 만듭니다.

첨부파일 생성 요청 실행

이제 교사가 첨부하려는 콘텐츠를 알았으므로 클래스룸 API에 요청을 실행하여 과제에 첨부파일을 만듭니다. 클래스룸 API에서 응답을 받은 후 데이터베이스에 첨부파일 세부정보를 저장합니다.

먼저 클래스룸 서비스의 인스턴스를 가져옵니다.

Python

제공된 예에서는 webapp/attachment_routes.py 파일에 있습니다.

def create_attachments(filename_caption_pairs):
    """
    Create attachments and show an acknowledgement page.

    Args:
        filename_caption_pairs: A dictionary that maps image filenames to
            captions.
    """
    # Get the Google Classroom service.
    classroom_service = googleapiclient.discovery.build(
        serviceName="classroom",
        version="v1",
        credentials=credentials)

CREATE 요청을 courses.courseWork.addOnAttachments 엔드포인트에 실행합니다. 교사가 선택한 각 이미지에 대해 먼저 AddOnAttachment 객체를 구성합니다.

Python

제공된 예에서는 create_attachments 메서드가 계속됩니다.

# Create a new attachment for each image that was selected.
attachment_count = 0
for key, value in filename_caption_pairs.items():
    attachment_count += 1

    # Create a dictionary with values for the AddOnAttachment object fields.
    attachment = {
        # Specifies the route for a teacher user.
        "teacherViewUri": {
            "uri":
                flask.url_for(
                    "load_content_attachment", _scheme='https', _external=True),
        },
        # Specifies the route for a student user.
        "studentViewUri": {
            "uri":
                flask.url_for(
                    "load_content_attachment", _scheme='https', _external=True)
        },
        # The title of the attachment.
        "title": f"Attachment {attachment_count}",
    }

각 첨부파일에 대해 teacherViewUri, studentViewUri, title 필드를 하나 이상 제공해야 합니다. teacherViewUristudentViewUri는 각 사용자 유형이 첨부파일을 열 때 로드되는 URL을 나타냅니다.

적절한 addOnAttachments 엔드포인트에 대한 요청의 본문에 AddOnAttachment 객체를 보냅니다. 각 요청에 courseId, itemId, itemType, addOnToken 식별자를 제공합니다.

Python

제공된 예에서는 create_attachments 메서드가 계속됩니다.

# Use the itemType to determine which stream item type the teacher created
match flask.session["itemType"]:
    case "announcements":
        parent = classroom_service.courses().announcements()
    case "courseWorkMaterials":
        parent = classroom_service.courses().courseWorkMaterials()
    case _:
        parent = classroom_service.courses().courseWork()

# Issue a request to create the attachment.
resp = parent.addOnAttachments().create(
    courseId=flask.session["courseId"],
    itemId=flask.session["itemId"],
    addOnToken=flask.session["addOnToken"],
    body=attachment).execute()

나중에 올바른 콘텐츠를 로드할 수 있도록 로컬 데이터베이스에 이 첨부파일의 항목을 만듭니다. 클래스룸은 생성 요청에 대한 응답으로 고유한 id 값을 반환하므로 이를 데이터베이스의 기본 키로 사용합니다. 또한 클래스룸은 교사 및 학생 뷰를 열 때 attachmentId 쿼리 매개변수를 전달합니다.

Python

제공된 예에서는 create_attachments 메서드가 계속됩니다.

# Store the value 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)
db.session.add(new_attachment)
db.session.commit()

이 시점에서 사용자를 확인 페이지로 라우팅하여 첨부파일을 성공적으로 만들었음을 알리는 것이 좋습니다.

부가기능의 첨부파일 허용

지금 Google Workspace Marketplace SDK의 앱 구성 페이지에 있는 허용된 첨부파일 URI 접두사 필드에 적절한 주소를 추가하는 것이 좋습니다. 부가기능은 이 페이지에 나열된 URI 접두사 중 하나에서만 첨부파일을 만들 수 있습니다. 이는 중간자 공격의 가능성을 줄이는 데 도움이 되는 보안 조치입니다.

가장 간단한 방법은 이 필드에 최상위 도메인을 제공하는 것입니다(예: https://example.com). https://localhost:<your port number>/ 로컬 머신을 웹 서버로 사용하는 경우 작동합니다.

교사 및 학생 뷰의 경로 추가

Google 클래스룸 부가기능이 로드될 수 있는 iframe은 4개입니다. 지금까지 첨부파일 검색 뷰 iframe을 제공하는 경로만 빌드했습니다. 다음으로 교사 및 학생 뷰 iframe을 제공하는 경로도 추가합니다.

교사 뷰 iframe은 학생 환경의 미리보기를 표시하는 데 필요하지만 추가 정보 또는 수정 기능을 선택적으로 포함할 수 있습니다.

학생 뷰 는 각 학생이 부가기능 첨부파일을 열 때 표시되는 페이지입니다.

이 연습에서는 교사 뷰와 학생 뷰를 모두 제공하는 단일 /load-content-attachment 경로를 만듭니다. 페이지가 로드될 때 클래스룸 API 메서드를 사용하여 사용자가 교사인지 학생인지 확인합니다.

Python

제공된 예에서는 webapp/attachment_routes.py 파일에 있습니다.

@app.route("/load-content-attachment")
def load_content_attachment():
    """
    Load the attachment for the user's role."""

    # Since this is a landing page for the Teacher and Student View iframes, we
    # need to preserve the incoming query parameters.
    if flask.request.args.get("itemId"):
        flask.session["itemId"] = flask.request.args.get("itemId")
    if flask.request.args.get("itemType"):
        flask.session["itemType"] = flask.request.args.get("itemType")
    if flask.request.args.get("courseId"):
        flask.session["courseId"] = flask.request.args.get("courseId")
    if flask.request.args.get("attachmentId"):
        flask.session["attachmentId"] = flask.request.args.get("attachmentId")

이 시점에서 사용자를 인증해야 합니다. 여기에서 login_hint 쿼리 매개변수도 처리하고 필요한 경우 사용자를 승인 흐름으로 라우팅해야 합니다. 이 흐름에 관한 자세한 내용은 이전 둘러보기에서 설명한 로그인 안내 세부정보를 참고하세요.

그런 다음 항목 유형과 일치하는 getAddOnContext 엔드포인트에 요청을 보냅니다.

Python

제공된 예에서는 load_content_attachment 메서드가 계속됩니다.

# Create an instance of the Classroom service.
classroom_service = googleapiclient.discovery.build(
    serviceName="classroom"
    version="v1",
    credentials=credentials)

# Use the itemType to determine which stream item type the teacher created
match flask.session["itemType"]:
    case "announcements":
        parent = classroom_service.courses().announcements()
    case "courseWorkMaterials":
        parent = classroom_service.courses().courseWorkMaterials()
    case _:
        parent = classroom_service.courses().courseWork()

addon_context_response = parent.getAddOnContext(
    courseId=flask.session["courseId"],
    itemId=flask.session["itemId"]).execute()

이 메서드는 수업에서 현재 사용자의 역할에 관한 정보를 반환합니다. 사용자의 역할에 따라 사용자에게 표시되는 뷰를 변경합니다. 응답 객체에서 studentContext 또는 teacherContext 필드 중 정확히 하나가 채워집니다. 이를 검사하여 사용자에게 어떻게 대처할지 결정합니다.

어떤 경우든 attachmentId 쿼리 매개변수 값을 사용하여 데이터베이스에서 검색할 첨부파일을 파악합니다. 이 쿼리 매개변수는 교사 또는 학생 뷰 URI를 열 때 제공됩니다.

Python

제공된 예에서는 load_content_attachment 메서드가 계속됩니다.

# Determine which view we are in by testing the returned context type.
user_context = "student" if addon_context_response.get(
    "studentContext") else "teacher"

# Look up the attachment in the database.
attachment = Attachment.query.get(flask.session["attachmentId"])

# Set the text for the next page depending on the user's role.
message_str = f"I see that you're a {user_context}! "
message_str += (
    f"I've loaded the attachment with ID {attachment.attachment_id}. "
    if user_context == "teacher" else
    "Please enjoy this image of a famous landmark!")

# Show the content with the customized message text.
return flask.render_template(
    "show-content-attachment.html",
    message=message_str,
    image_filename=attachment.image_filename,
    image_caption=attachment.image_caption,
    responses=response_strings)

부가기능 테스트

첨부파일 생성을 테스트하려면 다음 단계를 완료하세요.

  • 교사 테스트 사용자 중 한 명으로 [Google 클래스룸] 에 로그인합니다.
  • 수업 과제 탭으로 이동하여 새 과제 를 만듭니다.
  • 텍스트 영역 아래에 있는 부가기능 버튼을 클릭한 다음 부가기능을 선택합니다. iframe이 열리고 부가기능이 Google Workspace Marketplace SDK의 앱 구성 페이지에서 지정한 첨부파일 설정 URI를 로드합니다.
  • 과제에 첨부할 콘텐츠를 선택합니다.
  • 첨부파일 생성 흐름이 완료되면 iframe을 닫습니다.

Google 클래스룸의 과제 생성 UI에 첨부파일 카드가 표시됩니다. 카드를 클릭하여 교사 뷰 iframe을 열고 올바른 첨부파일이 표시되는지 확인합니다. 할당 버튼을 클릭합니다.

학생 환경을 테스트하려면 다음 단계를 완료하세요.

  • 그런 다음 교사 테스트 사용자와 동일한 수업에서 학생 테스트 사용자로 클래스룸에 로그인합니다.
  • 수업 과제 탭에서 테스트 과제를 찾습니다.
  • 과제를 펼치고 첨부파일 카드를 클릭하여 학생 뷰 iframe을 엽니다.

학생에게 올바른 첨부파일이 표시되는지 확인합니다.

축하합니다. 다음 단계인 활동 유형 첨부파일 만들기를 진행할 준비가 되었습니다.