Notes en pièce jointe et renvoi des notes

Il s'agit du sixième tutoriel de la série sur les modules complémentaires Classroom.

Dans ce tutoriel, vous allez modifier l'exemple de l'étape précédente pour produire une pièce jointe de type activité notée. Vous pouvez également renvoyer une note à Google Classroom de manière programmatique. Elle apparaît dans le carnet de notes de l'enseignant en tant que note provisoire.

Ce tutoriel diffère légèrement des autres de la série, car il présente deux approches possibles pour renvoyer les notes à Classroom. Les deux ont des impacts distincts sur les expériences des développeurs et des utilisateurs. Tenez compte des deux lorsque vous concevez votre module complémentaire Classroom. Pour en savoir plus sur les options d'implémentation, consultez notre page du guide sur l'interaction avec les pièces jointes.

Notez que les fonctionnalités de notation de l'API sont facultatives. Elles peuvent être utilisées avec n'importe quel type de pièce jointe d'activité.

Au cours de cette procédure pas à pas, vous allez effectuer les opérations suivantes :

  • Modifiez les requêtes de création de pièces jointes précédentes envoyées à l'API Classroom pour définir également le dénominateur de la note de la pièce jointe.
  • Notez le devoir de l'élève de manière programmatique et définissez le numérateur de la note de la pièce jointe.
  • Implémentez deux approches pour transmettre la note de l'exercice à Classroom à l'aide d'identifiants d'enseignant connectés ou hors connexion.

Une fois la correction terminée, les notes s'affichent dans le carnet de notes Classroom une fois le comportement de renvoi déclenché. Le moment exact où cela se produit dépend de l'approche d'implémentation.

Pour cet exemple, réutilisez l'activité du tutoriel précédent, dans laquelle une image d'un monument célèbre est présentée à un élève, qui est invité à saisir son nom. Attribuez la note maximale à la pièce jointe si l'élève saisit le bon nom, et zéro dans le cas contraire.

Comprendre la fonctionnalité de notation de l'API des modules complémentaires Classroom

Votre module complémentaire peut définir le numérateur et le dénominateur de la note d'une pièce jointe. Elles sont respectivement définies à l'aide des valeurs pointsEarned et maxPoints dans l'API. Une fiche de pièce jointe dans l'interface utilisateur de Classroom affiche la valeur maxPoints lorsqu'elle a été définie.

Exemple de plusieurs pièces jointes avec maxPoints sur un devoir

Figure 1. Interface utilisateur de création de devoirs avec trois fiches de pièces jointes de modules complémentaires pour lesquelles maxPoints est défini.

L'API Classroom Add-ons vous permet de configurer les paramètres et de définir la note obtenue pour les devoirs avec pièces jointes. Elles sont différentes des notes des devoirs. Toutefois, les paramètres de note du devoir suivent ceux de la pièce jointe portant le libellé Synchronisation des notes sur sa fiche. Lorsque la pièce jointe "Synchronisation des notes" définit pointsEarned pour un devoir envoyé par un élève, elle définit également la note provisoire de l'élève pour le devoir.

En général, la première pièce jointe ajoutée au devoir qui définit maxPoints reçoit le libellé "Synchronisation des notes". Pour obtenir un exemple du libellé "Synchronisation des notes", consultez l'exemple d'interface utilisateur de création de devoirs illustré à la figure 1. Notez que la fiche "Pièce jointe 1" comporte le libellé "Synchronisation des notes" et que la note de l'exercice dans l'encadré rouge a été mise à jour et est désormais de 50 points. Notez également que, bien que la figure 1 montre trois cartes de pièces jointes, une seule porte le libellé "Synchronisation des notes". Il s'agit d'une limite clé de l'implémentation actuelle : une seule pièce jointe peut porter le libellé "Synchronisation des notes".

Si plusieurs pièces jointes sont définies sur maxPoints, la suppression de la pièce jointe avec la synchronisation des notes n'active pas la synchronisation des notes sur les autres pièces jointes. Si vous ajoutez une autre pièce jointe qui définit maxPoints, la synchronisation des notes est activée pour la nouvelle pièce jointe et la note maximale du devoir est ajustée en conséquence. Il n'existe aucun mécanisme permettant de voir par programmation quel pièce jointe porte le libellé "Synchronisation des notes" ni le nombre de pièces jointes d'un devoir donné.

Définir la note maximale d'une pièce jointe

Cette section décrit comment définir le dénominateur d'une note de pièce jointe, c'est-à-dire le score maximal que tous les élèves peuvent obtenir pour leurs devoirs. Pour ce faire, définissez la valeur maxPoints de la pièce jointe.

Seule une modification mineure de notre implémentation existante est nécessaire pour activer les fonctionnalités de notation. Lorsque vous créez une pièce jointe, ajoutez la valeur maxPoints dans le même objet AddOnAttachment qui contient les champs studentWorkReviewUri, teacherViewUri et d'autres champs de pièce jointe.

Notez que la note maximale par défaut pour un devoir est de 100. Nous vous suggérons de définir maxPoints sur une valeur autre que 100 afin de pouvoir vérifier que les notes sont correctement définies. Définissez maxPoints sur 50 pour la démonstration :

Python

Ajoutez le champ maxPoints lorsque vous créez l'objet attachment, juste avant d'envoyer une requête CREATE au point de terminaison courses.courseWork.addOnAttachments. Vous le trouverez dans le fichier webapp/attachment_routes.py si vous suivez l'exemple fourni.

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

Pour les besoins de cette démonstration, vous stockez également la valeur maxPoints dans votre base de données locale "Attachment" (Pièce jointe). Cela vous évite d'avoir à effectuer un appel d'API supplémentaire ultérieurement lors de la notation des devoirs des élèves. Notez toutefois qu'il est possible que les enseignants modifient les paramètres de note des devoirs indépendamment de votre module complémentaire. Envoyez une requête GET au point de terminaison courses.courseWork pour afficher la valeur maxPoints au niveau du devoir. Pour ce faire, transmettez itemId dans le champ CourseWork.id.

Mettez à jour votre modèle de base de données pour qu'il contienne également la valeur maxPoints de la pièce jointe. Nous vous recommandons d'utiliser la valeur maxPoints de la réponse CREATE :

Python

Commencez par ajouter un champ max_points à la table Attachment. Vous trouverez cette information dans le fichier webapp/models.py si vous suivez notre exemple.

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

Revenez à la requête courses.courseWork.addOnAttachments CREATE. Stockez la valeur maxPoints renvoyée dans la réponse.

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

La pièce jointe a désormais une note maximale. Vous devriez pouvoir tester ce comportement maintenant : ajoutez une pièce jointe à un devoir, puis vérifiez que la fiche de la pièce jointe affiche le libellé "Synchronisation des notes" et que la valeur "Points" du devoir change.

Attribuer une note à un devoir d'élève dans Classroom

Cette section explique comment définir le numérateur pour la note d'un devoir, c'est-à-dire le score individuel d'un élève pour le devoir. Pour ce faire, définissez la valeur pointsEarned d'un devoir avec pièce jointe d'un élève.

Vous devez maintenant prendre une décision importante : comment votre module complémentaire doit-il envoyer une requête pour définir pointsEarned ?

Le problème est que le paramètre pointsEarned nécessite le champ d'application OAuth teacher. Vous ne devez pas accorder le champ d'application teacher aux élèves, car cela pourrait entraîner un comportement inattendu lorsqu'ils interagissent avec votre module complémentaire, par exemple en chargeant l'iframe de la vue de l'enseignant au lieu de celle de l'élève. Vous avez donc deux possibilités pour définir pointsEarned :

  • en utilisant les identifiants de l'enseignant connecté.
  • Utiliser des identifiants d'enseignant stockés (hors connexion)

Les sections suivantes présentent les avantages et les inconvénients de chaque approche avant de montrer comment les implémenter. Notez que les exemples fournis illustrent les deux approches pour transmettre une note à Classroom. Consultez les instructions spécifiques à chaque langage ci-dessous pour savoir comment sélectionner une approche lors de l'exécution des exemples fournis :

Python

Recherchez la déclaration SET_GRADE_WITH_LOGGED_IN_USER_CREDENTIALS en haut du fichier webapp/attachment_routes.py. Définissez cette valeur sur True pour renvoyer les notes à l'aide des identifiants de l'enseignant connecté. Définissez cette valeur sur False pour renvoyer les notes à l'aide des identifiants stockés lorsque l'élève envoie l'activité.

Définir des notes à l'aide des identifiants de l'enseignant connecté

Utilisez les identifiants de l'utilisateur connecté pour envoyer la requête permettant de définir pointsEarned. Cela devrait sembler assez intuitif, car cela reflète le reste de l'implémentation jusqu'à présent et ne nécessite que peu d'efforts pour être réalisé.

Toutefois, gardez à l'esprit que l'enseignant n'interagit qu'avec le devoir de l'élève dans l'iFrame "Examen des devoirs". Cela a des conséquences importantes :

  • Aucune note ne s'affiche dans Classroom tant que l'enseignant n'a pas effectué d'action dans l'interface utilisateur de Classroom.
  • Un enseignant peut être amené à ouvrir chaque devoir remis par les élèves pour saisir toutes les notes.
  • Il existe un bref délai entre la réception de la note par Classroom et son affichage dans l'interface utilisateur de Classroom. Le délai est généralement de cinq à dix secondes, mais il peut aller jusqu'à 30 secondes.

La combinaison de ces facteurs signifie que les enseignants peuvent avoir à effectuer un travail manuel considérable et chronophage pour remplir complètement les notes d'un cours.

Pour implémenter cette approche, ajoutez un appel d'API à votre route StudentWork Review existante.

Après avoir récupéré les enregistrements des devoirs et des pièces jointes des élèves, évaluez les devoirs des élèves et stockez la note obtenue. Définissez la note dans le champ pointsEarned d'un objet AddOnAttachmentStudentSubmission. Enfin, envoyez une requête PATCH au point de terminaison courses.courseWork.addOnAttachments.studentSubmissions avec l'instance AddOnAttachmentStudentSubmission dans le corps de la requête. Notez que nous devons également spécifier pointsEarned dans updateMask dans notre requête 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()

Attribuer des notes à l'aide d'identifiants d'enseignant hors connexion

La deuxième approche pour définir des notes nécessite l'utilisation d'identifiants stockés pour l'enseignant qui a créé la pièce jointe. Cette implémentation nécessite que vous construisiez des identifiants à l'aide des jetons d'actualisation et d'accès d'un enseignant autorisé précédemment, puis que vous utilisiez ces identifiants pour définir pointsEarned.

L'un des principaux avantages de cette approche est que les notes sont renseignées sans que l'enseignant n'ait à effectuer d'action dans l'interface utilisateur de Classroom, ce qui évite les problèmes mentionnés ci-dessus. Les utilisateurs finaux perçoivent ainsi l'expérience de notation comme fluide et efficace. Cette approche vous permet également de choisir le moment où vous renvoyez les notes, par exemple lorsque les élèves terminent l'activité ou de manière asynchrone.

Pour implémenter cette approche, effectuez les tâches suivantes :

  1. Modifiez les enregistrements de la base de données utilisateur pour stocker un jeton d'accès.
  2. Modifiez les enregistrements de la base de données des pièces jointes pour stocker un ID d'enseignant.
  3. Récupérez les identifiants de l'enseignant et (facultatif) créez une instance de service Classroom.
  4. Attribuez une note à un devoir.

Pour cette démonstration, définissez la note lorsque l'élève termine l'activité, c'est-à-dire lorsqu'il envoie le formulaire dans l'itinéraire "Vue de l'élève".

Modifier les enregistrements de la base de données utilisateur pour stocker le jeton d'accès

Deux jetons uniques sont nécessaires pour effectuer des appels d'API : le jeton d'actualisation et le jeton d'accès. Si vous avez suivi la série de tutoriels jusqu'à présent, le schéma de table User devrait déjà stocker un jeton d'actualisation. Il suffit de stocker le jeton d'actualisation lorsque vous n'effectuez des appels d'API qu'avec l'utilisateur connecté, car vous recevez un jeton d'accès dans le cadre du flux d'authentification.

Toutefois, vous devez désormais effectuer des appels en tant qu'utilisateur autre que celui connecté, ce qui signifie que le flux d'authentification n'est pas disponible. Vous devez donc stocker le jeton d'accès en même temps que le jeton d'actualisation. Mettez à jour le schéma de votre table User pour inclure un jeton d'accès :

Python

Dans l'exemple fourni, il se trouve dans le fichier 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())

Ensuite, mettez à jour tout code qui crée ou met à jour un enregistrement User pour stocker également le jeton d'accès :

Python

Dans l'exemple fourni, il se trouve dans le fichier 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()

Modifier les enregistrements de la base de données des pièces jointes pour stocker un ID d'enseignant

Pour définir une note pour une activité, appelez pointsEarned en tant qu'enseignant dans le cours. Vous pouvez procéder de plusieurs manières :

  • Stockez un mappage local des identifiants des enseignants aux ID de cours. Toutefois, notez que le même enseignant ne sera pas toujours associé à un cours en particulier.
  • Envoyez des requêtes GET au point de terminaison courses de l'API Classroom pour obtenir le ou les enseignants actuels. Interrogez ensuite les enregistrements des utilisateurs locaux pour trouver les identifiants d'enseignant correspondants.
  • Lorsque vous créez une pièce jointe de module complémentaire, stockez un ID d'enseignant dans la base de données locale des pièces jointes. Récupérez ensuite les identifiants de l'enseignant à partir de attachmentId transmis à l'iframe de la vue de l'élève.

Cet exemple illustre la dernière option, car vous définissez des notes lorsque l'élève termine une pièce jointe d'activité.

Ajoutez un champ d'ID d'enseignant à la table Attachment de votre base de données :

Python

Dans l'exemple fourni, il se trouve dans le fichier 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))

Mettez ensuite à jour tout code qui crée ou met à jour un enregistrement Attachment pour stocker également l'ID du créateur :

Python

Dans l'exemple fourni, il s'agit de la méthode create_attachments dans le fichier 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()

Récupérer les identifiants de l'enseignant

Recherchez l'itinéraire qui dessert l'iFrame de la vue de l'élève. Immédiatement après avoir stocké la réponse de l'élève dans votre base de données locale, récupérez les identifiants de l'enseignant à partir de votre stockage local. Cette étape devrait être simple, compte tenu de la préparation effectuée lors des deux étapes précédentes. Vous pouvez également les utiliser pour créer une instance du service Classroom pour l'utilisateur enseignant :

Python

Dans l'exemple fourni, il s'agit de la méthode load_activity_attachment dans le fichier 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)

Attribuer une note à un devoir

La procédure à suivre à partir de là est identique à celle de l'utilisation des identifiants de l'enseignant connecté. Toutefois, notez que vous devez effectuer l'appel avec les identifiants de l'enseignant récupérés à l'étape précédente :

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

Tester le module complémentaire

Comme dans la procédure pas à pas précédente, créez un devoir avec une pièce jointe de type activité en tant qu'enseignant, envoyez une réponse en tant qu'élève, puis ouvrez le devoir dans l'iframe "Examen des devoirs des élèves". Vous devriez pouvoir voir la note apparaître à différents moments selon votre approche d'implémentation :

  • Si vous avez choisi de renvoyer une note lorsque l'élève a terminé l'activité, vous devriez déjà voir sa note provisoire dans l'UI avant d'ouvrir l'iframe "Examen des travaux de l'élève". Vous pouvez également le voir dans la liste des élèves lorsque vous ouvrez le devoir, et dans la zone "Note" à côté de l'iframe "Vérification des devoirs des élèves".
  • Si vous avez choisi de renvoyer une note lorsque l'enseignant ouvre l'iFrame "Vérification des travaux des élèves", la note devrait apparaître dans la zone "Note" peu de temps après le chargement de l'iFrame. Comme indiqué ci-dessus, cela peut prendre jusqu'à 30 secondes. La note de l'élève concerné devrait également s'afficher dans les autres vues du carnet de notes Classroom.

Vérifiez que la note correcte s'affiche pour l'élève.

Félicitations ! Vous êtes prêt à passer à l'étape suivante : créer des pièces jointes en dehors de Google Classroom.