Notas de anexo e passback da nota

Este é o sexto tutorial da série de instruções de complementos do Google Sala de Aula.

Neste tutorial, você vai modificar o exemplo da etapa anterior para produzir um anexo do tipo de atividade classificado. Você também transmite uma nota para o Google Sala de Aula de maneira programática, e ela aparece no diário de classe do professor como uma nota temporária.

Este tutorial é um pouco diferente dos outros da série, porque há duas abordagens possíveis para transmitir notas para o Google Sala de Aula. Os dois tipos têm impactos diferentes na experiência do desenvolvedor e do usuário. Considere ambos ao criar seu complemento do Google Sala de Aula. Leia nossa página do guia de interação com anexos para uma discussão adicional das opções de implementação.

Os recursos de avaliação na API são opcionais. Elas podem ser usadas com qualquer anexo do tipo de atividade.

Neste tutorial, você vai fazer o seguinte:

  • Modifique as solicitações anteriores de criação de anexo para a API Classroom para definir também o denominador de nota do anexo.
  • Dê uma pontuação programática ao envio do estudante e defina o numerador de notas do anexo.
  • Implemente duas abordagens para passar a nota do trabalho para o Google Sala de Aula usando credenciais de professor conectado ou off-line.

Após a conclusão, as notas aparecem no boletim de notas do Google Sala de Aula depois que o comportamento de passback é acionado. O momento exato em que isso acontece depende da abordagem de implementação.

Para este exemplo, reutilize a atividade do tutorial anterior, em que um estudante recebe a imagem de um ponto de referência famoso e é solicitado a inserir o nome dele. Se o aluno digitar o nome correto, atribua zero a nota. Caso contrário, atribua zero.

Entender o recurso de avaliação da API de complementos do Google Sala de Aula

Seu complemento pode definir o numerador e o denominador de nota de um anexo. Elas são, respectivamente, definidas usando os valores pointsEarned e maxPoints na API. Um card de anexo na interface do Google Sala de Aula mostra o valor de maxPoints quando definido.

Exemplo de vários anexos com maxPoints em uma atividade

Figura 1. A interface de criação de atribuições com três cartões de anexo de complementos que têm maxPoints definido.

A API de complementos do Google Sala de Aula permite definir as configurações e a pontuação de anexo. Elas não são iguais às notas da atribuição. No entanto, as configurações de nota seguem as configurações de nota do anexo que tem o rótulo Sincronização de notas no cartão de anexo. Quando o anexo "Sincronização de notas" define pointsEarned para o envio de um estudante, ele também define a nota temporária dele para a atividade.

Normalmente, o primeiro anexo adicionado à atividade que define maxPoints recebe o rótulo "Sincronização de notas". Consulte o exemplo da interface de criação de atividades mostrado na Figura 1 para ver um exemplo do rótulo "Sincronização de notas". Observe que o cartão "Anexo 1" tem o rótulo "Sincronização de notas" e que a nota da tarefa na caixa vermelha foi atualizada para 50 pontos. Observe também que, embora a Figura 1 mostre três cartões de anexo, apenas um deles tem o rótulo "Sincronização de notas". Essa é uma limitação importante da implementação atual: apenas um anexo pode ter o rótulo "Sincronização de notas".

Se houver vários anexos com maxPoints definido, a remoção dele com "Sincronização de notas" não ativará a "Sincronização de notas" em nenhum dos anexos restantes. Adicionar outro anexo que define maxPoints ativa a sincronização de notas no novo anexo, e a nota máxima da atividade é ajustada de acordo. Não há mecanismo para ver programaticamente qual anexo tem o rótulo "Sincronização de notas" nem para ver quantos anexos uma atividade específica tem.

Definir a nota máxima de um anexo

Esta seção descreve a definição do denominador de uma nota de anexo, ou seja, a pontuação máxima possível que todos os alunos podem atingir nos envios. Para fazer isso, defina o valor maxPoints do anexo.

É necessária apenas uma pequena modificação na implementação atual para ativar os recursos de avaliação. Ao criar um anexo, adicione o valor maxPoints no mesmo objeto AddOnAttachment que contém studentWorkReviewUri, teacherViewUri e outros campos de anexo.

A pontuação máxima padrão de uma nova atividade é 100. Sugerimos definir maxPoints como um valor diferente de 100 para que você possa verificar se as notas estão sendo definidas corretamente. Defina maxPoints como 50 como demonstração:

Python

Adicione o campo maxPoints ao criar o objeto attachment, antes de emitir uma solicitação CREATE para o endpoint courses.courseWork.addOnAttachments. Você pode encontrá-lo no arquivo webapp/attachment_routes.py se estiver seguindo o exemplo fornecido.

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}",
}

Para esta demonstração, você também armazena o valor maxPoints no banco de dados de anexos local. Isso economiza a necessidade de fazer uma chamada de API extra mais tarde durante a avaliação dos envios dos alunos. No entanto, é possível que os professores alterem as configurações de nota das tarefas independentemente do complemento. Envie uma solicitação GET ao endpoint courses.courseWork para conferir o valor maxPoints no nível da atribuição. Ao fazer isso, transmita o itemId no campo CourseWork.id.

Agora atualize seu modelo de banco de dados para também armazenar o valor maxPoints do anexo. Recomendamos usar o valor maxPoints da resposta CREATE:

Python

Primeiro, adicione um campo max_points à tabela Attachment. Se estiver seguindo o exemplo fornecido, você o encontrará no arquivo 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)

Retorne à solicitação CREATE courses.courseWork.addOnAttachments. Armazene o valor maxPoints retornado na resposta.

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

Agora o anexo tem uma nota máxima. Agora você pode testar esse comportamento: adicione um anexo a uma nova atividade e observe que o cartão de anexo mostra o rótulo "Sincronização de notas" e o valor de "Pontos" da atividade muda.

Definir a nota de envio dos estudantes no Google Sala de Aula

Esta seção descreve a configuração do numerador de uma nota anexada, ou seja, a pontuação de um estudante específico no anexo. Para fazer isso, defina o valor pointsEarned do envio de um estudante.

Agora você tem uma decisão importante a tomar: como seu complemento vai enviar uma solicitação para definir pointsEarned?

O problema é que a configuração de pointsEarned exige o escopo teacher do OAuth. Não conceda o escopo teacher para usuários estudantes. Isso pode resultar em comportamentos inesperados quando os estudantes interagem com seu complemento, como carregar o iframe da Visualização de professores em vez do iframe da visualização do estudante. Portanto, você tem duas opções para definir pointsEarned:

  • Usando as credenciais do professor que fez login.
  • Usar credenciais de professor armazenadas (off-line).

As seções a seguir discutem as vantagens e desvantagens de cada abordagem antes de demonstrar cada implementação. Os exemplos fornecidos demonstram as duas abordagens para passar notas no Google Sala de Aula. Consulte as instruções específicas do idioma abaixo para saber como selecionar uma abordagem ao executar os exemplos fornecidos:

Python

Encontre a declaração SET_GRADE_WITH_LOGGED_IN_USER_CREDENTIALS na parte superior do arquivo webapp/attachment_routes.py. Defina esse valor como True para transmitir notas usando as credenciais do professor que fez login. Defina esse valor como False para transmitir notas usando credenciais armazenadas quando o estudante enviar a atividade.

Definir notas usando as credenciais do professor que fez login

Use as credenciais do usuário conectado para emitir a solicitação e definir pointsEarned. Isso deve parecer bastante intuitivo, já que espelha o restante da implementação e requer pouco esforço para ser percebido.

No entanto, considere que o professor interage apenas com o envio do aluno no iframe "Revisão dos trabalhos dos alunos". Isso tem algumas implicações importantes:

  • Nenhuma nota é preenchida no Google Sala de Aula até que o professor execute uma ação na interface do Google Sala de Aula.
  • Um professor pode ter que abrir todos os envios de alunos para preencher todas as notas deles.
  • Há um breve atraso entre o recebimento da nota e a exibição dela na interface do Sala de Aula. O atraso normalmente é de cinco a dez segundos, mas pode ser de até 30 segundos.

A combinação desses fatores significa que talvez os professores precisem fazer um trabalho manual considerável e demorado para preencher totalmente as notas de uma turma.

Para implementar essa abordagem, adicione mais uma chamada de API ao trajeto de avaliação dos trabalhos dos alunos.

Depois de buscar os registros de envio e anexo do aluno, avalie o envio do aluno e armazene a nota resultante. Defina a nota no campo pointsEarned de um objeto AddOnAttachmentStudentSubmission. Por fim, emita uma solicitação PATCH para o endpoint courses.courseWork.addOnAttachments.studentSubmissions com a instância AddOnAttachmentStudentSubmission no corpo da solicitação. Também precisamos especificar pointsEarned no updateMask da solicitação PATCH:

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

Definir notas usando credenciais de professor off-line

A segunda abordagem para definir notas requer o uso de credenciais armazenadas para o professor que criou o anexo. Essa implementação exige que você crie credenciais usando tokens de atualização e acesso de um professor autorizado anteriormente e, em seguida, use essas credenciais para definir pointsEarned.

Uma vantagem essencial dessa abordagem é que as notas são preenchidas sem exigir ação do professor na interface do Google Sala de Aula, o que evita os problemas mencionados acima. O resultado é que os usuários finais consideram a experiência de avaliação como um processo simples e eficiente. Além disso, essa abordagem permite escolher o momento em que você passa as notas, por exemplo, quando os estudantes concluem a atividade ou de forma assíncrona.

Conclua as tarefas a seguir para implementar essa abordagem:

  1. Modifique os registros do banco de dados do usuário para armazenar um token de acesso.
  2. Modifique os registros do banco de dados de anexos para armazenar um ID de professor.
  3. Recupere as credenciais do professor e, se quiser, crie uma nova instância de serviço do Google Sala de Aula.
  4. Definir a nota de uma atividade.

Para esta demonstração, defina a nota quando o aluno concluir a atividade, ou seja, quando ele enviar o formulário na rota "Visualização do estudante".

Modifique os registros do banco de dados do usuário para armazenar o token de acesso

São necessários dois tokens exclusivos para fazer chamadas de API: o token de atualização e o token de acesso. Se você tem seguido a série de tutoriais até agora, o esquema de tabela User já deve armazenar um token de atualização. Armazenar o token de atualização é suficiente quando você só faz chamadas de API com o usuário conectado, porque recebe um token de acesso como parte do fluxo de autenticação.

No entanto, agora você precisa fazer chamadas como alguém que não seja o usuário conectado, o que significa que o fluxo de autenticação não está disponível. Portanto, você precisa armazenar o token de acesso junto com o token de atualização. Atualize o esquema da tabela User para incluir um token de acesso:

Python

No exemplo fornecido, isso está no arquivo 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())

Em seguida, atualize qualquer código que crie ou atualize um registro User para também armazenar o token de acesso:

Python

No exemplo fornecido, isso está no arquivo 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()

Modificar registros do banco de dados de anexos para armazenar um ID de professor

Se quiser dar a nota a uma atividade, chame pointsEarned como professor do curso. Há várias maneiras de fazer isso:

  • Armazenar um mapeamento local das credenciais de professores para os IDs dos cursos. No entanto, nem sempre o mesmo professor está associado a um curso específico.
  • Envie solicitações GET ao endpoint courses da API Classroom para receber os professores atuais. e os registros de usuários locais para localizar as credenciais de professor correspondentes.
  • Ao criar um anexo de complemento, armazene um ID de professor no banco de dados de anexos locais. Em seguida, recupere as credenciais de professor do attachmentId transmitido para o iframe da visualização dos alunos.

Este exemplo demonstra a última opção, já que você está definindo notas quando o aluno conclui um anexo de atividade.

Adicione um campo de ID de professor à tabela Attachment do banco de dados:

Python

No exemplo fornecido, isso está no arquivo 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))

Em seguida, atualize qualquer código que crie ou atualize um registro Attachment para também armazenar o ID do criador:

Python

No exemplo fornecido, isso está no método create_attachments no arquivo webapp/attachment_routes.py.

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

Recuperar as credenciais do professor

Encontre o trajeto que veicula o iframe do Student View. Imediatamente após armazenar a resposta do aluno no banco de dados local, recupere as credenciais do professor no armazenamento local. Isso deve ser simples devido à preparação nas duas etapas anteriores. Você também pode usá-los para criar uma nova instância do serviço Sala de Aula para o usuário professor:

Python

No exemplo fornecido, isso está no método load_activity_attachment no arquivo webapp/attachment_routes.py.

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

Definir a nota de um envio

O procedimento é o mesmo de usar as credenciais do professor que fez login. No entanto, faça a chamada com as credenciais de professor recuperadas na etapa anterior:

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

Testar o complemento

Assim como no tutorial anterior, crie uma atividade com um anexo de tipo de atividade como professor, envie uma resposta como estudante e abra o envio no iframe "Revisão dos trabalhos dos alunos". A nota aparece em momentos diferentes, dependendo da abordagem de implementação:

  • Se você optar por transmitir uma nota quando o estudante concluir a atividade, já verá a nota temporária na interface antes de abrir o iframe "Revisão dos trabalhos dos alunos". Também é possível vê-la na lista de alunos ao abrir a tarefa e na caixa "Nota" ao lado do iframe "Revisão dos trabalhos dos alunos".
  • Se você optar por transmitir uma nota quando o professor abrir o iframe "Revisão dos trabalhos dos alunos", ela aparecerá na caixa "Nota" logo após o carregamento do iframe. Conforme mencionado acima, isso pode levar até 30 segundos. Depois disso, a nota do estudante específico também vai aparecer nas outras visualizações do boletim de notas do Google Sala de Aula.

Confirme se aparece a pontuação correta para o estudante.

Parabéns! Está tudo pronto para a próxima etapa: criar anexos fora do Google Sala de Aula.