Notas de anexo e passback da nota

Este é o sexto tutorial da série de complementos do Google Sala de Aula.

Neste tutorial, você vai modificar o exemplo da etapa anterior para produzir um anexo de tipo de atividade com nota. Você também envia uma nota de volta para o Google Sala de Aula de maneira programática, que 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 retornar as notas ao Google Sala de Aula. Ambos têm impactos distintos nas experiências do desenvolvedor e do usuário. Considere esses dois elementos ao criar seu complemento do Google Sala de Aula. Leia a página Guia de interação com anexos para mais detalhes sobre as opções de implementação.

Os recursos de avaliação na API são opcionais. Eles podem ser usados com qualquer anexo de tipo de atividade.

Neste tutorial, você vai fazer o seguinte:

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

Depois que o comportamento de retorno é acionado, as notas aparecem no boletim de notas do Google Sala de Aula. O momento exato em que isso acontece depende da abordagem de implementação.

Neste exemplo, reutilize a atividade do tutorial anterior, em que um estudante vê a imagem de um ponto de referência famoso e precisa inserir o nome dele. Atribua notas completas ao anexo se o aluno digitar o nome correto. Caso contrário, escreva "0".

Sobre 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. Eles são definidos respectivamente 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 ele foi definido.

Exemplo de vários anexos com maxPoints em uma
atividade

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

A API de complementos do Google Sala de Aula permite definir as configurações e a pontuação recebida pelas notas de anexo. Elas não são iguais às notas de atribuição. No entanto, as configurações de nota da atividade seguem as configurações de nota de anexo 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 aluno, ele também define a nota temporária do estudante na atividade.

Normalmente, o primeiro anexo adicionado à atividade que define maxPoints recebe o marcador "Sincronização de notas". Confira o exemplo da interface de criação de atividades mostrado na Figura 1 para um exemplo do marcador "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 tem o rótulo "Sincronização de notas". Essa é uma limitação importante da implementação atual: apenas um anexo pode ter o marcador "Sincronização de notas".

Se houver vários anexos com a configuração maxPoints definida, a remoção do anexo com a opção "Sincronização de notas" não ativará a opção "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 para corresponder. 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 que todos os alunos podem atingir nos envios. Para isso, defina o valor maxPoints do anexo.

É necessária apenas uma pequena modificação na nossa 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 para 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, logo antes de emitir uma solicitação CREATE para o endpoint courses.courseWork.addOnAttachments. Você poderá encontrar isso no arquivo webapp/attachment_routes.py se seguir nosso 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 outra chamada de API posteriormente durante a avaliação dos envios dos estudantes. No entanto, é possível que os professores alterem as configurações de notas da tarefa independentemente do seu complemento. Envie uma solicitação GET para o endpoint courses.courseWork para ver o valor de maxPoints no nível da atribuição. Ao fazer isso, transmita itemId no campo CourseWork.id.

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

Python

Primeiro, adicione um campo max_points à tabela Attachment. Você poderá encontrá-lo no arquivo webapp/models.py se seguir nosso exemplo fornecido.

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

Volte para a solicitação courses.courseWork.addOnAttachments CREATE. 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 do anexo mostra o rótulo "Sincronização de notas" e o valor de "Pontos" da atividade muda.

Definir a nota para os estudantes enviarem no Google Sala de Aula

Nesta seção, descrevemos a configuração do numerador para uma nota de anexo, ou seja, a pontuação de um aluno individual para o anexo. Para fazer isso, defina o valor de pointsEarned do envio de um estudante.

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

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

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

As seções a seguir discutem as vantagens e desvantagens de cada abordagem antes de demonstrar cada implementação. Nossos exemplos demonstram ambas as abordagens para passar uma nota 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 as notas usando as credenciais do professor conectado. Defina esse valor como False para transmitir as notas usando as 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 reflete o restante da implementação até agora e requer pouco esforço para ser realizado.

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

  • As notas só serão preenchidas depois que o professor realizar uma ação na interface do produto.
  • Pode ser que um professor precise abrir todos os envios de estudantes para preencher todas as notas.
  • Há um pequeno atraso entre o recebimento da nota pelo Google Sala de Aula e a exibição dela na interface do Google Sala de Aula. O atraso geralmente é de 5 a 10 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 à rota de avaliação dos trabalhos dos estudantes.

Depois de buscar os registros de envio do estudante e anexos, 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 na 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 professores off-line

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

Uma vantagem fundamental dessa abordagem é que as notas são preenchidas sem exigir a ação do professor na interface do Google Sala de Aula, evitando os problemas mencionados acima. O resultado é que os usuários finais percebem a experiência de avaliação como contínua e eficiente. Além disso, essa abordagem permite escolher o momento em que você devolve as notas, como quando os alunos concluem a atividade ou de forma assíncrona.

Conclua as seguintes tarefas 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, opcionalmente, crie uma nova instância do serviço Sala de aula.
  4. Defina a nota de um envio.

Para esta demonstração, defina a nota quando o estudante concluir a atividade, ou seja, quando ele enviar o formulário pelo link de 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 de acesso. Se você tem seguido a série de tutoriais até agora, seu esquema de tabela User já deve armazenar um token de atualização. Armazenar o token de atualização é suficiente se você só faz chamadas de API com o usuário conectado, já que recebe um token de acesso como parte do fluxo de autenticação.

No entanto, agora você precisa fazer chamadas como outra pessoa 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

Em nosso 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

Em nosso 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

Para definir a nota de uma atividade, faça uma chamada para definir pointsEarned como professor do curso. Há várias maneiras de fazer isso:

  • Armazene 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.
  • Emita solicitações GET para o endpoint courses da API Classroom para acessar os professores atuais. Em seguida, consulte os registros de usuários locais para encontrar as credenciais correspondentes dos professores.
  • 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 estudantes.

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 do professor à tabela Attachment do seu banco de dados:

Python

Em nosso 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

Em nosso 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, dado a preparação nas duas etapas anteriores. Também é possível usá-los para criar uma nova instância do serviço Google Sala de Aula para o usuário professor:

Python

Em nosso 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 é idêntico ao usar as credenciais do professor que fez login. No entanto, faça a chamada com as credenciais do 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 de avaliação dos trabalhos dos alunos. A nota vai aparecer em momentos diferentes, dependendo da abordagem de implementação:

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

Confirme se a pontuação correta é exibida para o aluno.

Parabéns! Você já pode prosseguir para a próxima etapa: criar anexos fora do Google Sala de Aula.