Este é o sexto tutorial da série de tutoriais dos 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. Ambos têm impactos distintos nas experiências do desenvolvedor e do usuário. Considere os dois ao projetar seu complemento do Google Sala de Aula. Leia nossa página de guia sobre como interagir com anexos para mais informações sobre as 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.
- Avalie a atividade do aluno de forma programática e defina o numerador da nota do anexo.
- Implemente duas abordagens para transmitir a nota do envio para o Google Sala de Aula usando credenciais de professores off-line ou de login.
Depois de concluídas, as notas aparecem no boletim do Google Sala de Aula depois que o comportamento de retorno é acionado. O momento exato em que isso acontece depende da abordagem de implementação.
Para fins deste exemplo, reutilize a atividade do tutorial anterior, em que um estudante recebe uma imagem de um ponto turístico famoso e é instruído 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 dos complementos do Google Sala de Aula
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 é definido.
Figura 1. A interface de criação de atividades com três cards de anexos de complementos que
têm maxPoints
definidos.
A API de complementos do Google Sala de Aula permite definir as configurações e a pontuação
das notas de anexo. Elas não são iguais às notas da
tarefa. 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 um
envio do estudante, ele também define a nota do rascunho do aluno para a atividade.
Normalmente, o primeiro anexo adicionado à atividade que define
maxPoints
recebe o rótulo "Sincronização de nota". Confira o exemplo de interface de criação de tarefas
mostrado na Figura 1 para conferir um exemplo do rótulo "Grade sync". O card "Anexo 1" tem o rótulo "Sincronização de nota", e 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 anexo, apenas um tem o rótulo "Grade sync". 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
, remover o
anexo com "Grade sync" não ativa a "Grade sync" em nenhum dos
anexos restantes. Adicionar outro anexo que defina maxPoints
ativa a
sincronização de notas no novo anexo, e a nota máxima da atividade é ajustada para
combinar. Não há um mecanismo para saber de forma programática qual anexo tem o
rótulo "Grade sync" nem para saber 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 nas
envios. Para 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 os campos 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 fins de 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 encontrar isso no
arquivo webapp/attachment_routes.py
se seguir 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 fins de 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 as inscrições dos estudantes. No entanto, é possível que os professores alterem as configurações de nota das tarefas independentemente do complemento. Envie
uma solicitação GET
para o 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 o modelo do banco de dados para armazenar também 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 o 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)
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 card de anexo mostra o rótulo "Sincronização de nota" e o valor de "Pontos" da atividade muda.
Definir a nota de envio dos estudantes 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 específico para o 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 deve emitir uma
solicitação para definir pointsEarned
?
O problema é que a configuração pointsEarned
requer o escopo OAuth teacher
.
Não conceda o escopo teacher
a usuários estudantes. Isso pode resultar em
comportamento inesperado quando os estudantes interagem com seu complemento, como o carregamento do
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 que fez login.
- Usar credenciais de professor armazenadas (off-line).
As seções a seguir discutem as compensações de cada abordagem antes de demonstrar cada implementação. Os exemplos fornecidos demonstram ambas as abordagens para transmitir uma nota ao Google Sala de Aula. Confira as instruções específicas de cada 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 enviar
notas usando as credenciais do professor conectado. Defina esse valor como False
para enviar as notas usando as credenciais armazenadas quando o aluno enviar a
atividade.
Definir notas usando as credenciais do professor conectado
Use as credenciais do usuário que fez login 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, o professor só interage com o envio do aluno no iframe de análise do trabalho do estudante. Isso tem algumas implicações importantes:
- As notas não são preenchidas no Google Sala de Aula até que o professor tome uma ação na interface do app.
- Um professor pode precisar abrir cada envio para preencher todas as notas dos estudantes.
- Há um pequeno 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 chegar a 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 seu trajeto de avaliação de trabalhos dos alunos.
Depois de buscar os registros de envio e anexo do estudante, avalie o
envio 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 off-line do professor
A segunda abordagem para definir as 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 anteriormente e, em seguida, use essas credenciais para definir pointsEarned
.
Uma vantagem importante 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 simples e eficiente. Além disso, essa abordagem permite que você escolha o momento em que vai enviar as notas, como quando os estudantes concluem a atividade ou de forma assíncrona.
Conclua as tarefas a seguir para implementar essa abordagem:
- Modifique os registros do banco de dados do usuário para armazenar um token de acesso.
- Modifique os registros do banco de dados de anexos para armazenar um ID de professor.
- Recupere as credenciais do professor e, se quiser, crie uma nova instância de serviço do Google Sala de Aula.
- Definir a nota de uma atividade.
Para fins de demonstração, defina a nota quando o estudante concluir a atividade, ou seja, quando ele 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 esquema da 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, é 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
professor no curso. Há várias maneiras de fazer isso:
- Armazene um mapeamento local das credenciais dos professores para os IDs dos cursos. No entanto, o mesmo professor nem sempre está associado a um curso específico.
- Emita solicitações
GET
para o endpointcourses
da API Classroom para receber os professores atuais. Em seguida, consulte os registros de usuários locais para localizar as credenciais de professores correspondentes. - Ao criar um anexo de complemento, armazene um ID de professor no banco de dados de anexos
locais. Em seguida, extraia 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 aluno 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()
Extrair 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, considerando a 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,
credentials=teacher_credentials)
Definir a nota de uma atividade
O procedimento a partir daqui é idêntico ao de usar as credenciais do professor autenticado. 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
Semelhante ao tutorial anterior, crie uma atividade com um anexo do tipo atividade como professor, envie uma resposta como estudante e abra o envio no iframe de análise do trabalho do estudante. 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 do estudante". 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. 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 nota correta aparece para o estudante.
Parabéns! Você já pode passar para a próxima etapa: criar anexos fora do Google Sala de Aula.