Notes en pièce jointe et renvoi des notes

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

Dans ce tutoriel, vous allez modifier l'exemple de l'étape précédente pour générer un rattachement de type d'activité noté. Vous pouvez également renvoyer une note à Google Classroom par programmation, qui apparaît sous forme de note temporaire dans le carnet de notes de l'enseignant.

Ce tutoriel diffère légèrement des autres de la série, car il existe deux approches possibles pour transmettre les notes à Classroom. Les deux ont un impact distinct sur l'expérience des développeurs et sur l'expérience utilisateur. Tenez-en compte lors de la conception de votre module complémentaire Classroom. Pour en savoir plus sur les options d'implémentation, consultez la page du guide Interagir avec les pièces jointes.

Notez que les fonctionnalités de notation de l'API sont facultatives. Ils peuvent être utilisés avec n'importe quel rattachement de type d'activité.

Dans ce tutoriel, vous allez effectuer les opérations suivantes:

  • Modifiez les requêtes de création de pièces jointes précédentes dans l'API Classroom pour définir également le dénominateur des notes de la pièce jointe.
  • Évaluez de manière programmatique le travail de l'élève et définissez le numérateur de note de la pièce jointe.
  • Mettez en œuvre deux approches pour transmettre la note du devoir à Classroom à l'aide des identifiants des enseignants connectés ou hors connexion.

Une fois l'opération terminée, les notes apparaissent 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 les besoins de cet exemple, réutilisez l'activité du tutoriel précédent, dans lequel un élève voit l'image d'un point de repère célèbre et est invité à saisir son nom. Attribuez des notes complètes pour la pièce jointe si l'élève a saisi 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 à la fois le numérateur des notes et le dénominateur des notes pour 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 indique la valeur maxPoints lorsqu'elle a été définie.

Exemple de pièces jointes avec maxPoints sur un devoir

Figure 1. UI de création d'attributions avec trois cartes de pièce jointe de module complémentaire pour lesquelles maxPoints est défini

L'API des modules complémentaires Classroom vous permet de configurer les paramètres et de définir le score obtenu pour les notes associées aux pièces jointes. Il s'agit de notes différentes des notes du devoir. Toutefois, les paramètres de notation d'un devoir suivent ceux de la pièce jointe dont la fiche comporte le libellé Synchronisation des notes. Lorsque la pièce jointe "Synchronisation des notes" définit pointsEarned pour un devoir, la note temporaire de l'élève pour le devoir est également définie.

En règle générale, la première pièce jointe ajoutée au devoir qui définit maxPoints reçoit le libellé "Synchronisation des notes". Reportez-vous à l'exemple d'interface utilisateur de création de devoir illustré à la figure 1 pour voir un exemple de libellé "Synchronisation des notes". Notez que la fiche "Pièce jointe 1" porte le libellé "Synchronisation des notes" et que la note du devoir dans le cadre rouge a été redéfinie sur 50 points. Notez également que bien que la figure 1 montre trois fiches de pièces jointes, seule une fiche porte le libellé "Synchronisation des notes". Il s'agit d'une limite clé de l'implémentation actuelle: une seule pièce jointe peut avoir le libellé "Synchronisation des notes".

Si plusieurs pièces jointes ont la valeur maxPoints, la suppression de la pièce jointe via la synchronisation des notes n'active pas la synchronisation des notes pour les pièces jointes restantes. L'ajout d'une autre pièce jointe qui définit maxPoints active la synchronisation des notes sur la nouvelle pièce jointe, et la note maximale du devoir s'ajuste en conséquence. Il n'existe aucun mécanisme permettant de voir par programmation les pièces jointes associées au libellé "Synchronisation des notes" ni le nombre de pièces jointes associées à un devoir donné.

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

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

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 et teacherViewUri, ainsi que d'autres champs de la pièce jointe.

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

Python

Ajoutez le champ maxPoints lors de la construction de 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 allez également stocker la valeur maxPoints dans votre base de données locale de pièces jointes, ce qui 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 notation d'un devoir 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 de l'attribution. Lors de cette opération, transmettez itemId dans le champ CourseWork.id.

À présent, mettez à jour votre modèle de base de données pour qu'il contienne également la valeur maxPoints du rattachement. Nous vous recommandons d'utiliser la valeur maxPoints de la réponse CREATE:

Python

Tout d'abord, ajoutez un champ max_points à la table Attachment. Vous le trouverez dans le fichier webapp/models.py si vous suivez l'exemple fourni.

# 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. Ajoutez une pièce jointe à un nouveau devoir et notez que la fiche de la pièce jointe affiche le libellé "Synchronisation des notes" et que la valeur "Points" du devoir change.

Définir la note attribuée à un élève à un envoi dans Classroom

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

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

Le problème est que le paramètre pointsEarned requiert le champ d'application OAuth teacher. Vous ne devez pas accorder le niveau d'accès teacher aux élèves. Cela peut entraîner un comportement inattendu lorsque les élèves interagissent avec votre module complémentaire, par exemple en chargeant l'iFrame de la vue enseignant au lieu de l'iFrame de la Vue des élèves. Vous avez donc deux choix pour définir pointsEarned:

  • Avec les identifiants de l'enseignant connecté.
  • Avec les identifiants d'enseignants stockés (hors connexion).

Les sections suivantes décrivent les avantages et les inconvénients propres à chaque approche avant de présenter chaque mise en œuvre. Notez que les exemples fournis illustrent les deux approches pour transmettre une note à Classroom. Consultez les instructions propres à 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 transmettre les notes à l'aide des identifiants de l'enseignant connecté. Définissez cette valeur sur False pour transmettre 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 émettre la requête de définition de pointsEarned. Cette approche devrait sembler assez intuitive, car elle reflète le reste de l'implémentation et nécessite peu d'efforts.

Toutefois, n'oubliez pas que l'enseignant n'interagit que avec le devoir envoyé par l'élève dans l'iFrame "Examen des devoirs". Cela a des implications importantes:

  • Aucune note n'est insérée dans Classroom tant que l'enseignant n'intervient pas dans l'interface utilisateur de Classroom.
  • Un enseignant peut être amené à ouvrir le devoir de chaque élève pour renseigner toutes les notes des élèves.
  • Un bref délai s'écoule entre la réception de la note dans Classroom et son affichage dans l'interface utilisateur de Classroom. Le délai est généralement de cinq à dix secondes, mais peut aller jusqu'à 30 secondes.

La combinaison de ces facteurs signifie que les enseignants devront peut-être effectuer des tâches manuelles considérables et chronophages pour renseigner entièrement les notes d'un cours.

Pour implémenter cette approche, ajoutez un appel d'API supplémentaire à votre parcours existant pour l'évaluation des devoirs.

Après avoir récupéré le travail de l'élève et les enregistrements des pièces jointes, évaluez le travail envoyé par l'élève 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 le updateMask de 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()

Définir des notes à l'aide des identifiants d'enseignant hors connexion

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

L'un des principaux avantages de cette approche est que les notes s'affichent sans nécessiter d'action de la part de l'enseignant dans l'interface utilisateur de Classroom, ce qui permet d'éviter les problèmes mentionnés ci-dessus. Les utilisateurs finaux perçoivent ainsi l'expérience de notation comme fluide et efficace. De plus, cette approche vous permet de choisir le moment auquel vous souhaitez transmettre 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 des utilisateurs pour y stocker un jeton d'accès.
  2. Modifiez les enregistrements de la base de données "Attachment" pour y stocker un ID d'enseignant.
  3. Récupérez les identifiants de l'enseignant et, éventuellement, créez une instance de service Classroom.
  4. Définissez la note d'un envoi.

Pour les besoins de cette démonstration, définissez la note lorsque l'élève termine l'activité, c'est-à-dire lorsqu'il envoie le formulaire via la route Vue des élèves.

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

Deux jetons uniques sont requis 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, votre schéma de table User doit déjà stocker un jeton d'actualisation. Le stockage du jeton d'actualisation est suffisant lorsque vous effectuez des appels d'API uniquement avec l'utilisateur connecté, car vous recevez un jeton d'accès dans le cadre du flux d'authentification.

Toutefois, vous devez maintenant passer des appels en tant qu'utilisateur autre que l'utilisateur connecté, ce qui signifie que le flux d'authentification n'est pas disponible. Vous devez donc stocker le jeton d'accès avec le jeton d'actualisation. Mettez à jour votre schéma de 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 "Attachment" pour y stocker un ID d'enseignant

Pour attribuer une note à une activité, appelez pointsEarned en tant qu'enseignant du cours. Il y a plusieurs façons d'y parvenir:

  • Stockez un mappage local entre les identifiants des enseignants et les ID de cours. Notez toutefois qu'un même enseignant ne peut pas toujours être associé à un cours 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 utilisateur locaux pour trouver les identifiants des enseignants correspondants.
  • Lorsque vous créez une pièce jointe de module complémentaire, stockez un ID enseignant dans la base de données des rattachements locaux. Ensuite, récupérez les identifiants de l'enseignant à partir du attachmentId transmis à l'iFrame de la vue des élèves.

Cet exemple illustre la dernière option, puisque vous définissez des notes lorsque l'élève remplit 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))

Ensuite, mettez à 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, cette méthode se trouve dans la méthode create_attachments du 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 affiche l'iFrame de la vue des élèves. 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. Cela devrait être simple compte tenu de la préparation 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, cette méthode se trouve dans la méthode load_activity_attachment du 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,
    discoveryServiceUrl=f"https://classroom.googleapis.com/$discovery/rest?labels=ADD_ONS_ALPHA&key={GOOGLE_API_KEY}",
    credentials=teacher_credentials)

Définir la note d'un devoir

La procédure à suivre est identique à celle qui permet d'utiliser les identifiants de l'enseignant connecté. Toutefois, notez que vous devez effectuer l'appel en utilisant 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 le tutoriel précédent, créez un devoir avec une pièce jointe de type d'activité en tant qu'enseignant, envoyez une réponse en tant qu'élève, puis ouvrez le devoir dans l'iFrame "Examen des devoirs". Vous devriez pouvoir voir la note apparaître à différents moments en fonction de votre approche d'implémentation:

  • Si vous avez choisi de rendre une note lorsque l'élève a terminé l'activité, vous devriez déjà voir sa note temporaire dans l'interface utilisateur avant d'ouvrir l'iFrame de l'évaluation des devoirs. Il apparaît également dans la liste des élèves à l'ouverture du devoir, ainsi que dans la zone "Note" à côté de l'iFrame "Examen des devoirs".
  • Si vous avez choisi de rendre une note lorsque l'enseignant ouvre l'iFrame "Devoirs des élèves", la note doit s'afficher dans la zone "Note" peu de temps après le chargement de l'iFrame. Comme indiqué ci-dessus, cette opération peut prendre jusqu'à 30 secondes. Par la suite, la note de l'élève concerné devrait également apparaître dans les autres vues du carnet de notes Classroom.

Vérifiez que le bon score 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.