Calificaciones de archivos adjuntos y devoluciones de calificaciones

Esta es la sexta explicación de la serie sobre complementos de Classroom.

En esta explicación, modificarás el ejemplo del paso anterior para producir un archivo adjunto de tipo de actividad calificado. También puedes transferir una calificación a Google Classroom de manera programática, lo que aparece en el libro de calificaciones del profesor como una calificación preliminar.

Esta explicación se diferencia ligeramente de otras en la serie en que existen dos enfoques posibles para aprobar las calificaciones de vuelta a Classroom. Ambos tienen impactos distintos en las experiencias del desarrollador y del usuario. Ten en cuenta ambos cuando diseñes el complemento de Classroom. Lee nuestra página de la guía Cómo interactuar con archivos adjuntos para obtener un análisis adicional de las opciones de implementación.

Ten en cuenta que las funciones de calificación de la API son opcionales. Pueden usarse con cualquier adjunto de tipo de actividad.

En esta explicación, completarás lo siguiente:

  • Modifica las solicitudes de creación de archivos adjuntos anteriores a la API de Classroom para establecer también el denominador de calificación de los archivos adjuntos.
  • Califica de manera programática la entrega del estudiante y configura el numerador de las calificaciones del adjunto.
  • Implementa dos enfoques para pasar la calificación de la entrega a Classroom con credenciales de profesor que hayan accedido a su cuenta o no estén conectados.

Cuando termines, las calificaciones aparecerán en el libro de calificaciones de Classroom luego de que se active el comportamiento de devoluciones. El momento exacto en el que esto suceda dependerá del enfoque de implementación.

A los efectos de este ejemplo, vuelve a usar la actividad de la explicación anterior, en la que al estudiante se le muestra la imagen de un punto de referencia famoso y se le solicita que ingrese su nombre. Asigna marcas completas para el adjunto si el estudiante ingresa el nombre correcto, de lo contrario, cero.

Comprender la función de calificación de la API de complementos de Classroom

Tu complemento puede configurar el numerador y el denominador de las calificaciones para un archivo adjunto. Estos se configuran, respectivamente, con los valores pointsEarned y maxPoints en la API. Una tarjeta adjunta en la IU de Classroom muestra el valor maxPoints una vez configurado.

Ejemplo de varios adjuntos con maxPoints en una asignación

Figura 1. La IU de creación de tareas con tres tarjetas adjuntas de complementos que tienen configurado maxPoints

La API de complementos de Classroom te permite definir la configuración y la puntuación que obtuviste para las calificaciones de archivos adjuntos. Estas no son lo mismo que las calificaciones de las tareas. Sin embargo, la configuración de calificación de las tareas sigue la configuración de calificación de los archivos adjuntos que tienen la etiqueta Sincronización de calificaciones en su tarjeta de archivo adjunto. Cuando el archivo adjunto “Sincronización de calificaciones” establece pointsEarned para la entrega de un estudiante, también establece la calificación preliminar del estudiante para la tarea.

Por lo general, el primer adjunto que se agregó a la tarea que establece maxPoints recibe la etiqueta "Sincronización de calificaciones". Consulta el ejemplo de la IU de creación de tareas que se muestra en la Figura 1 para ver un ejemplo de la etiqueta "Sincronización de calificaciones". Ten en cuenta que la tarjeta “Archivo adjunto 1” tiene la etiqueta “Sincronización de calificaciones” y que la calificación de la tarea en el cuadro rojo se actualizó a 50 puntos. Además, ten en cuenta que, aunque en la Figura 1 se muestran tres tarjetas de archivos adjuntos, solo una tiene la etiqueta "Sincronización de calificaciones". Esta es una limitación clave de la implementación actual: solo un adjunto puede tener la etiqueta "Grade sync".

Si hay varios archivos adjuntos que establecieron maxPoints, quitar el archivo adjunto con "Sincronización de calificaciones" no habilita la "Sincronización de calificaciones" en ninguno de los archivos adjuntos restantes. Si agregas otro archivo adjunto que establezca maxPoints, se habilitará la sincronización de calificaciones en el archivo adjunto nuevo, y la calificación máxima de las tareas se ajustará para que coincida. No existe un mecanismo para ver de manera programática qué adjunto tiene la etiqueta "Sincronización de calificaciones" ni para ver cuántos archivos adjuntos tiene una tarea en particular.

Cómo establecer la calificación máxima de un archivo adjunto

En esta sección, se describe cómo configurar el denominador para una calificación de archivo adjunto, es decir, la puntuación máxima posible que todos los estudiantes pueden alcanzar en sus entregas. Para hacerlo, configura el valor maxPoints del archivo adjunto.

Solo se necesita una pequeña modificación en nuestra implementación existente para habilitar las funciones de calificación. Cuando crees un adjunto, agrega el valor maxPoints en el mismo objeto AddOnAttachment que contiene los campos studentWorkReviewUri, teacherViewUri y otros campos de archivos adjuntos.

Ten en cuenta que la puntuación máxima predeterminada para una tarea nueva es de 100. Te sugerimos que establezcas maxPoints en un valor distinto de 100 para que puedas verificar que las calificaciones se establezcan de forma correcta. Establece maxPoints en 50 como demostración:

Python

Agrega el campo maxPoints cuando construyas el objeto attachment, justo antes de emitir una solicitud CREATE al extremo courses.courseWork.addOnAttachments. Puedes encontrarlo en el archivo webapp/attachment_routes.py si sigues el ejemplo proporcionado.

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 los fines de esta demostración, también almacenas el valor maxPoints en tu base de datos de archivos adjuntos local, lo que evita tener que realizar una llamada a la API adicional más adelante cuando calificas las entregas de los estudiantes. Sin embargo, ten en cuenta que es posible que los profesores modifiquen la configuración de calificaciones de las tareas de forma independiente del complemento. Envía una solicitud GET al extremo courses.courseWork para ver el valor maxPoints a nivel de la asignación. Cuando lo hagas, pasa itemId en el campo CourseWork.id.

Ahora, actualiza tu modelo de base de datos para que también conserve el valor maxPoints del adjunto. Recomendamos usar el valor maxPoints de la respuesta CREATE:

Python

Primero, agrega un campo max_points a la tabla Attachment. Puedes encontrarlo en el archivo webapp/models.py si sigues nuestro ejemplo proporcionado.

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

Regresa a la solicitud CREATE courses.courseWork.addOnAttachments. Almacena el valor de maxPoints que se muestra en la respuesta.

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

Ahora el archivo adjunto tiene una calificación máxima. Deberías poder probar este comportamiento ahora. Para ello, agrega un archivo adjunto a una tarea nueva y observa que la tarjeta de archivo adjunto muestra la etiqueta "Sincronización de calificaciones" y los cambios en los valores de "Puntos" de la tarea.

Cómo establecer la calificación de entrega de un alumno en Classroom

En esta sección, se describe cómo configurar el numerador para una calificación del archivo adjunto, es decir, la calificación de un archivo adjunto para un estudiante individual. Para hacerlo, establece el valor de pointsEarned de un estudiante.

Ahora tienes una decisión importante que tomar: ¿cómo debe tu complemento emitir una solicitud para configurar pointsEarned?

El problema es que la configuración de pointsEarned requiere el permiso de OAuth de teacher. No debes otorgarles el permiso teacher a los usuarios estudiantes, ya que esto podría provocar un comportamiento inesperado cuando los estudiantes interactúen con tu complemento, como cargar el iframe de la vista de profesor en lugar del iframe de la vista de estudiante. Por lo tanto, tienes dos opciones para configurar pointsEarned:

  • Usar las credenciales del profesor que accedió
  • Usar credenciales de profesor almacenadas (sin conexión)

En las siguientes secciones, se analizan las ventajas y desventajas de cada enfoque antes de demostrar cada implementación. Ten en cuenta que los ejemplos proporcionados demuestran ambos enfoques para aprobar una calificación en Classroom. Consulta las instrucciones específicas para cada lenguaje que aparecen a continuación si quieres ver cómo seleccionar un enfoque cuando ejecutas los ejemplos proporcionados:

Python

Busca la declaración SET_GRADE_WITH_LOGGED_IN_USER_CREDENTIALS en la parte superior del archivo webapp/attachment_routes.py. Establece este valor en True para aprobar las calificaciones con las credenciales del profesor que accedió. Establece este valor en False para aprobar las calificaciones con las credenciales almacenadas cuando el estudiante envíe la actividad.

Establece las calificaciones con las credenciales del profesor que accedió

Usa las credenciales del usuario que accedió a fin de emitir la solicitud para configurar pointsEarned. Esto debería parecer bastante intuitivo, ya que refleja el resto de la implementación hasta ahora y requiere poco esfuerzo.

Sin embargo, ten en cuenta que el profesor solo interactúa con la entrega del alumno en el iframe de Revisión del trabajo de los estudiantes. Esto tiene algunas implicaciones importantes:

  • Las calificaciones no se propagan en Classroom hasta que el profesor toma medidas en la IU de Classroom.
  • Es posible que un profesor deba abrir la entrega de cada estudiante para propagar todas sus calificaciones.
  • Hay un breve retraso entre la recepción de la calificación y el momento en que Classroom aparece en la IU de Classroom. Por lo general, la demora es de cinco a diez segundos, pero puede ser de hasta 30 segundos.

La combinación de estos factores implica que los profesores pueden tener que realizar un trabajo manual considerable y lento para completar las calificaciones de una clase.

Para implementar este enfoque, agrega una llamada a la API adicional a la ruta existente de la Revisión del trabajo de los estudiantes.

Después de recuperar los registros de entregas y archivos adjuntos del estudiante, evalúa su entrega y almacena la calificación resultante. Configura la calificación en el campo pointsEarned de un objeto AddOnAttachmentStudentSubmission. Por último, emite una solicitud PATCH al extremo courses.courseWork.addOnAttachments.studentSubmissions con la instancia AddOnAttachmentStudentSubmission en el cuerpo de la solicitud. Ten en cuenta que también debemos especificar pointsEarned en el updateMask de nuestra solicitud 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()

Establece calificaciones con credenciales de profesor sin conexión

El segundo enfoque para establecer calificaciones requiere el uso de credenciales almacenadas para el profesor que creó el adjunto. Esta implementación requiere que crees credenciales con tokens de acceso y actualización de un profesor previamente autorizado y, luego, uses estas credenciales para configurar pointsEarned.

Una ventaja fundamental de este enfoque es que las calificaciones se propagan sin necesidad de que los profesores realicen alguna acción en la IU de Classroom, lo que evita los problemas mencionados anteriormente. Como resultado, los usuarios finales perciben la experiencia de calificación como fluida y eficiente. Además, este enfoque te permite elegir el momento en el que apruebas las calificaciones, como cuando los estudiantes completan la actividad o de forma asíncrona.

Completa las siguientes tareas para implementar este enfoque:

  1. Modificar los registros de la base de datos de usuarios para almacenar un token de acceso
  2. Modificar los registros de la base de datos de archivos adjuntos para almacenar un ID de profesor
  3. Recuperar las credenciales del profesor y, de forma opcional, construir una nueva instancia de servicio de Classroom
  4. Establece la calificación de una entrega.

A los fines de esta demostración, establece la calificación cuando el estudiante complete la actividad, es decir, cuando envíe el formulario en la ruta Vista de estudiante.

Modifica los registros de la base de datos de usuarios para almacenar el token de acceso

Se requieren dos tokens únicos para realizar llamadas a la API: el token de actualización y el token de acceso. Si has seguido la serie de explicaciones hasta ahora, el esquema de la tabla User ya debería almacenar un token de actualización. Almacenar el token de actualización es suficiente cuando solo realizas llamadas a la API con el usuario que accedió, ya que recibes un token de acceso como parte del flujo de autenticación.

Sin embargo, ahora debes realizar llamadas con una persona que no sea el usuario que accedió, lo que significa que el flujo de autenticación no está disponible. Por lo tanto, debes almacenar el token de acceso junto con el token de actualización. Actualiza el esquema de tabla User para incluir un token de acceso:

Python

En nuestro ejemplo proporcionado, esto se encuentra en el archivo 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())

Luego, actualiza cualquier código que cree o actualice un registro User para almacenar también el token de acceso:

Python

En nuestro ejemplo proporcionado, esto se encuentra en el archivo 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()

Cómo modificar los registros de bases de datos de archivos adjuntos para almacenar un ID de profesor

Si deseas establecer una calificación para una actividad, realiza una llamada para definir a pointsEarned como profesor en el curso. Existen varias formas de hacerlo:

  • Almacena una asignación local de credenciales de profesores para los IDs de cursos. Sin embargo, ten en cuenta que el mismo profesor no siempre puede estar asociado a un curso en particular.
  • Emite solicitudes GET al extremo courses de la API de Classroom para obtener a los profesores actuales. Luego, consulta los registros de usuarios locales para encontrar las credenciales de profesor que coincidan.
  • Cuando crees un archivo adjunto de complemento, almacena un ID de profesor en la base de datos local de adjuntos. Luego, recupera las credenciales de profesor del attachmentId que se pasó al iframe de vista de alumno.

En este ejemplo, se muestra la última opción, ya que estás configurando las calificaciones cuando el estudiante completa un archivo adjunto de actividad.

Agrega un campo de ID de profesor a la tabla Attachment de tu base de datos:

Python

En nuestro ejemplo proporcionado, esto se encuentra en el archivo 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))

Luego, actualiza cualquier código que cree o actualice un registro Attachment para almacenar también el ID del creador:

Python

En nuestro ejemplo proporcionado, esto se encuentra en el método create_attachments del archivo 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 las credenciales del profesor

Busca la ruta que entrega el iframe de vista de alumno. Inmediatamente después de almacenar la respuesta del estudiante en la base de datos local, recupera las credenciales del profesor desde el almacenamiento local. Esto debería ser sencillo, dada la preparación de los dos pasos anteriores. También puedes usarlas para crear una instancia nueva del servicio de Classroom para el usuario profesor:

Python

En nuestro ejemplo proporcionado, esto se encuentra en el método load_activity_attachment del archivo 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)

Cómo configurar la calificación de una entrega

El procedimiento que sigue es idéntico al de usar las credenciales del profesor que accedió. Sin embargo, ten en cuenta que debes realizar la llamada con las credenciales de profesor recuperadas en el paso 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()

Prueba el complemento

Al igual que en la explicación anterior, crea una tarea con un archivo adjunto del tipo de actividad como profesor, envía una respuesta como estudiante y, luego, abre el envío en el iframe de revisión del trabajo de los estudiantes. Deberías poder ver la calificación en diferentes momentos según el enfoque de implementación:

  • Si elegiste aprobar una calificación cuando el estudiante completó la actividad, ya deberías ver su calificación preliminar en la IU antes de abrir el iframe de revisión del trabajo de los estudiantes. También puedes verla en la lista de estudiantes cuando abres la tarea y en el cuadro "Calificación" junto al iframe de revisión de trabajos de los estudiantes.
  • Si decides aprobar una calificación cuando el profesor abre el iframe de la Revisión del trabajo de los estudiantes, la calificación debería aparecer en el cuadro "Calificación" poco después de que se cargue el iframe. Como se mencionó anteriormente, este proceso puede tardar hasta 30 segundos. Luego, la calificación del estudiante específico también debería aparecer en las otras vistas del libro de calificaciones de Classroom.

Confirma que aparece la puntuación correcta para el estudiante.

¡Felicitaciones! Ya puedes continuar con el siguiente paso: crear archivos adjuntos fuera de Google Classroom.