Notas de anexo e passback da nota

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

Neste tutorial, você vai modificar o exemplo da etapa anterior para produzir um anexo de atividade com nota. Você também vai transmitir uma nota ao Google Sala de Aula de maneira programática, que aparece no diário de classe do professor como uma nota rascunho.

Este tutorial é um pouco diferente dos outros da série, porque apresenta duas abordagens possíveis para transmitir notas ao Google Sala de Aula. Ambas têm impactos distintos nas experiências do desenvolvedor e do usuário. Considere as duas ao criar seu complemento do Google Sala de Aula. Leia nossa página do guia Interagir com anexos para mais informações 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 atividade.

Ao longo deste tutorial, você vai concluir as seguintes etapas:

  • Modificar as solicitações de criação de anexos anteriores para a API Classroom para definir também o denominador da nota do anexo.
  • Avaliar de maneira programática o envio do estudante e definir o numerador da nota do anexo.
  • Implementar duas abordagens para transmitir a nota do envio ao Google Sala de Aula usando credenciais de professor conectado ou off-line.

Depois de concluídas, as notas aparecem no diário de classe do Google Sala de Aula após o acionamento do comportamento de transmissão. 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 uma imagem de um monumento famoso e é solicitado a inserir o nome dele. Atribua a nota máxima ao anexo se o estudante inserir o nome correto, caso contrário, atribua zero.

Entender o recurso de avaliação da API Classroom Add-ons

Seu complemento pode definir o numerador e o denominador da 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 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 cards de anexos de complementos que têm maxPoints definido.

A API Classroom Add-ons permite configurar as definições e definir a pontuação recebida para as notas de anexos. Elas não são as mesmas que as notas de atividades. No entanto, as configurações de notas de atividades seguem as configurações de notas de anexos do anexo que tem o rótulo Sincronização de notas no card de anexo. Quando o anexo "Sincronização de notas" define pointsEarned para um envio de estudante, ele também define a nota rascunho do estudante para a atividade.

Normalmente, o primeiro anexo adicionado à atividade que define maxPoints recebe o rótulo "Sincronização de notas". Consulte o exemplo de interface de criação de atividades mostrado na Figura 1 para conferir um exemplo do rótulo "Sincronização de notas". Observe que o card "Anexo 1" tem o rótulo "Sincronização de notas" e que a nota da atividade na caixa vermelha foi atualizada para 50 pontos. Observe também que, embora a Figura 1 mostre três cards de anexos, apenas um card 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 que definiram maxPoints, a remoção do anexo com "Sincronização de notas" não vai ativar a "Sincronização de notas" em nenhum dos anexos restantes. A adição de 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á um mecanismo para verificar de maneira programática qual anexo tem o rótulo "Sincronização de notas" nem para verificar quantos anexos uma atividade específica tem.

Definir a nota máxima de um anexo

Esta seção descreve como definir o denominador de uma nota de anexo, ou seja, a pontuação máxima possível que todos os estudantes podem alcançar para os envios. Para fazer isso, defina o valor maxPoints do anexo.

Apenas uma pequena modificação na nossa implementação atual é necessária para ativar os recursos de avaliação. Ao criar um anexo, adicione o valor maxPoints em o mesmo objeto AddOnAttachment que contém o 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 para demonstração:

Python

Adicione o maxPoints campo ao construir o attachment objeto, logo antes de emitir uma CREATE solicitação para o courses.courseWork.addOnAttachments endpoint. Você pode encontrar isso no arquivo webapp/attachment_routes.py se seguir nosso exemplo.

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 fins desta demonstração, você também armazena o valor maxPoints no banco de dados de anexos local. Isso evita a necessidade de fazer outra chamada de API mais tarde ao avaliar os envios dos estudantes. No entanto, é possível que os professores alterem as configurações de notas de atividades de maneira independente do seu complemento. Envie uma GET solicitação para o courses.courseWork endpoint para conferir o valor maxPoints da atividade. Ao fazer isso, transmita o itemId no campo CourseWork.id.

Agora atualize o 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. Você pode encontrar isso no arquivo webapp/models.py se seguir nosso exemplo.

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

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

Definir uma nota de envio de estudante no Google Sala de Aula

Esta seção descreve como definir o numerador de uma nota de anexo, ou seja, a pontuação de um estudante individual para o anexo. Para fazer isso, defina o valor pointsEarned do envio de um anexo de estudante.

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

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

  • Usando as credenciais do professor conectado.
  • Usando 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. Nossos exemplos fornecidos demonstram ambas as abordagens para transmitir uma nota ao 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 de cima do arquivo webapp/attachment_routes.py. Defina esse valor como True para transmitir notas usando as credenciais do professor conectado. Defina esse valor como False para transmitir notas usando credenciais armazenadas quando o estudante enviar a atividade.

Definir notas usando as credenciais do professor conectado

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

No entanto, considere que o professor interage com o envio do estudante no iframe de revisão do trabalho do estudante. Isso tem algumas implicações importantes:

  • Nenhuma nota é preenchida no Google Sala de Aula até que o professor tome medidas na interface do Google Sala de Aula.
  • Um professor pode precisar abrir todos os envios dos estudantes para preencher todas as notas.
  • Há um breve atraso entre o recebimento da nota pelo Google Sala de Aula e a aparência dela na interface do Google Sala de Aula. O atraso é normalmente de cinco a dez segundos, mas pode chegar a 30 segundos.

A combinação desses fatores significa que os professores podem precisar 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 revisão do trabalho do estudante.

Depois de buscar os registros de envio e anexo do estudante, avalie o envio do estudante e armazene a nota resultante. Defina a nota no pointsEarned campo de um AddOnAttachmentStudentSubmission objeto. Por fim, emita uma solicação PATCH para o courses.courseWork.addOnAttachments.studentSubmissions endpoint com a AddOnAttachmentStudentSubmission instância no corpo da solicitação. Observe que também precisamos especificar pointsEarned no updateMask na nossa 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 exige o uso de credenciais armazenadas para o professor que criou o anexo. Essa implementação exige que você construa credenciais usando os tokens de atualização e acesso de um professor autorizado anteriormente e, em seguida, use essas credenciais para definir pointsEarned.

Uma vantagem fundamental dessa abordagem é que as notas são preenchidas sem exigir 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 perfeita e eficiente. Além disso, essa abordagem permite escolher o momento em que você transmite as notas, como quando os estudantes concluem a atividade ou de forma assíncrona.

Conclua as tarefas a seguir para implementar essa abordagem:

  1. Modificar os registros do banco de dados do usuário para armazenar um token de acesso.
  2. Modificar os registros do banco de dados de anexos para armazenar um ID de professor.
  3. Recuperar as credenciais do professor e (opcionalmente) construir uma nova instância do serviço do Google Sala de Aula.
  4. Definir a nota de um envio.

Para fins desta demonstração, defina a nota quando o estudante concluir a atividade, ou seja, quando o estudante enviar o formulário na rota da visualização do estudante.

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

Dois tokens exclusivos são necessários para fazer chamadas de API: o token de atualização e o token de acesso. Se você seguiu a série de tutoriais até agora, o User esquema da tabela já deve armazenar um token de atualização. O armazenamento do token de atualização é suficiente quando você faz chamadas de API apenas com o usuário conectado, já que você recebe um token de acesso como parte do fluxo de autenticação.

No entanto, agora você precisa fazer chamadas como alguém diferente do usuário conectado, o que significa que o fluxo de autenticação não está disponível. Assim, é necessário 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 os registros do banco de dados de anexos para armazenar um ID de professor

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

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

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

Adicione um campo de ID de professor à tabela Attachment do seu 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 a rota que veicula o iframe da visualização do estudante. Imediatamente após armazenar a resposta do estudante no banco de dados local, recupere as credenciais do professor do armazenamento local. Isso deve ser simples, considerando a preparação nas duas etapas anteriores. Você também pode usá-las para construir uma nova instância do serviço do Google 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,
    credentials=teacher_credentials)

Definir a nota de um envio

O procedimento a partir daqui é idêntico ao de usar as credenciais do professor conectado. No entanto, observe que você precisa fazer 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

Semelhante ao tutorial anterior, crie uma atividade com um anexo de atividade como professor, envie uma resposta como estudante e abra o envio no iframe de revisão do trabalho do estudante. Você poderá conferir a nota em momentos diferentes, dependendo da abordagem de implementação:

  • Se você optou por transmitir uma nota quando o estudante concluiu a atividade, já verá a nota rascunho na interface antes de abrir o iframe de revisão do trabalho do estudante. Você também pode conferir na lista de estudantes ao abrir a atividade e na caixa "Nota" ao lado do iframe de revisão do trabalho do estudante.
  • Se você optou por transmitir uma nota quando o professor abre o iframe de revisão do trabalho do estudante, a nota vai aparecer na caixa "Nota" logo após o carregamento do iframe. Como 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 diário de classe do Google Sala de Aula.

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

Parabéns! Você está pronto para passar para a próxima etapa: criar anexos fora do Google Sala de Aula.