Moodle: Massen-Unenrolment bei globalen Gruppen?

Hallo.
Neuer Tag – neues Problem.

Ich habe bisher moosh verwendet, um Kurse zu resetten. Das lief bisher wunderbar und hat genau das getan, was hier ganz unten beschrieben ist:

(Leider funktioniert unser Script über die REST-API aus dem verlinkten Beitrag im Moment nicht mehr richtig – sonst würde ich mit dem Ansatz weitermachen)

Im letzten Durchgang haben wir die Kurs-Einschreibungen mit globalen Gruppen anstelle von manueller Einschreibung vorgenommen (für alle Klassen und Kurse der Unter-/Mittel-/Oberstufe).
Der Befehl
moosh -n course-reset -s "unenrol_users=5," 519
scheint daher keine Wirkung auf das Unenrolment von Kurs 519 zu haben, da er offenbar nur manuelle User-Einschreibungen aus den Kursen rückgängig macht :thinking: :interrobang:

Nun ist also die nächste moodle-Frage: Wie bekommt man einen sauberen Massen-Kurs-Reset mit Unenrolment aller User (aber nicht Lehrer!) hin, wenn globale Gruppen verwendet wurden?

Die Frage ist übrigens deshalb relevant, da die Versetzung auf dem v7-Server schon gelaufen ist und daher im moodle-Kurs der alten Klasse 6a jetzt die Schüler der neuen 6a sind. Da aber die Zuordnung unter moodle über Profile läuft, passen die Schüler nicht mehr zu den Kursen des alten Schuljahres. Daher würde ich gerne aus den alten Kursen alle Schüler (aber nicht die Lehrer) entfernen, um ein Archiv zu ermöglichen, auf das die Kollegen noch zugreifen können.

Vielleicht hat ja jemand eine gute Idee.
Viele Grüße,
Michael

Hallo Michael,

da gibt es mehrere sinnvolle Ansätze.

Zum einen kannst Du einfach eine Globale Gruppe löschen und neu anlegen.
Dann wird sie (eventuell muss man ein paar Cronjob-Durchläufe abwarten)
bei den Einschreibemethoden entfernt. Anschließend kannst Du die Globale
Gruppe neu anlegen.

Ein anderer Weg ist über den Upload von Kurslisten. Dabei kann man auch
bestehende Kurse modifizieren, insbesondere kann man einzelne
Einschreibemethoden löschen. Du kannst so auch die Lehrerinnen und Lehre
in die Kurse einschreiben. Dieser Listen-Upload ist sehr mächtig und
auch gut dokumentiert.

Viele Grüße

Jörg

Hallo.
Ok – beide Methoden kenne ich aber bei beiden gibt es gewisse Stolpersteine, meine ich:

Die globalen Gruppen der Klassen kann ich nicht löschen, da sie über Plugins abgeglichen werden. Den Papierkorb-Symbol gibt es entsprechend auch gar nicht in der Übersicht. Es ginge auf diesem Weg also nur bei manuell erzeugten globalen Gruppen. Und da ist der Workflow natürlich etwas schräg, wenn man alte Gruppen eigentlich loswerden will, sie aber zunächst neu anlegen soll, um sie dann zu löschen.

Der Weg über den Upload der Kurslisten habe ich bei der Einrichtung der aktuellen Kurse ja auch verwendet. Dort könnte man tatsächlich die alten Einschreibemethoden bestehender Kurse verändern. Dazu muss man aber in den alten CSV-Kurslisten nachträglich für vergangene Kurse etwas ändern und sie dann neu hochladen, wenn ich das richtig sehe. Der Weg könnte tatsächlich funktionieren (auch wenn ich ihn nicht sehr elegant finde).

Ich habe das alte Script, das ein ehemaliger Schüler von uns vor zwei Jahren im Lockdown geschrieben/erweitert hatte, noch hier: Das funktionierte schon mal und macht theoretisch genau das, was ich brauche:
Es greift über die REST-API auf moodle zu und dieser Weg ist vermutlich mit Abstand der eleganteste für solche Massen-Aktionen :thinking:.

Die Einzelschritte sind klar, doch reichen meine python-Kenntnisse leider nicht aus, um das wieder in Gang zu bringen. An irgendeiner Stelle klemmte es zuletzt (… ich bin aber nicht mehr sicher, ob es nur daran lag, dass die Verbindung da noch über den v6.x-Server lief)

Wenn sich jemand von Euch daran versuchen möchte: Gerne! Hier nochmal der Link zum Projekt:

und hier das python-Script archive.py:

#   That's the plan
#   
#   [classes 5 - 10]
#   1.) Rename and move Courses from 5-10 to archive
#   2.) unenrol all students, keep trainers
#   3.) create new courses in correct categories
#   4.) enrol stundents and trainers


import datetime
import re
import moodle
import categorytree
import logger

URL = 'https://meine-domain.de/moodle'
ENDPOINT = '/webservice/rest/server.php'
TOKEN = '11111111111111'

TRAINER_SHORTNAME = 'editingteacher'
STUDENT_SHORTNAME = 'student'

LAST_YEAR = datetime.date.today().year - 1


ARCHIVE_CATEGORY_NAME = "Archiv"

CLASS_YEARS = [5, 6, 7, 8, 9, 10, 11, 12, 13]

m = moodle.Moodle(URL + ENDPOINT, TOKEN)


# ----------- Helper -------------
def getYearFromClass(name):
    g = re.match('.*?([0-9]+)', name)
    if g is None:
        return None
    else:
        return g.group(0)


def isLowerLevelCourse(c: any):
    shortname = c['shortname']
    if (shortname and len(shortname) <= 3):
        classYear = getYearFromClass(shortname)
        if classYear and classYear.isdigit():
            return int(classYear) <= 10
    return False


def is11(c: any):
    if len(c['shortname']) == 4 and re.match('[1-9][a-z][a-z][1-9]', c['shortname']) is not None:
        return True
    return False


def is12_OR_13(c: any):
    if re.match('[A-z][A-z][1-9][1-9][1-9]', c['shortname']) is not None:
        return True
    return False

def is12(c: any):
    if str(LAST_YEAR - 7) in c['idnumber']:
        return True
    return False


def isUpperLevelCourse(c: any):
    return is11(c) or is12_OR_13(c)


def unenrolAllStudentsFromCourse(courseId):
    courseUsers = m.course_get_enroled_user(courseId)
    unenrolInstructions = []

    for user in courseUsers:
        if user['roles'][0]['shortname'] == STUDENT_SHORTNAME:
            unenrolInstructions.append({'userid': user['id'], 'courseid': course['id']})
    if len(unenrolInstructions) > 0:
        m.course_unenrol_users(unenrolInstructions)


def archiveCourse(course):
    newCategoryId = categorytree.mapCategoryGraphs(CATEGORY_ROOT_NODE, ARCHIVE_ROOT_NODE, course['categoryid']).data['id']
    print(course['categoryid'])
    print(newCategoryId)
    m.course_update(course['id'], categoryid=newCategoryId,
                                    fullname=course['fullname'] + ARCHIVE_COURSE_REPRODUCTION_APPENDIX,
                                    shortname=course['shortname'] + ARCHIVE_COURSE_REPRODUCTION_APPENDIX,
                                    idnumber=course['shortname'] + ARCHIVE_COURSE_REPRODUCTION_APPENDIX)
# -----------------------------------------------------------------------------------


logger.heading('Moodle archive script')
logger.job('Reading all courses...')
########################## All sorts of courses ######################################
ALL_OLD_COURSES = m.courses_get()

ALL_LOWER_LEVEL_COURSES = [c for c in ALL_OLD_COURSES if isLowerLevelCourse(c)]

ALL_UPPER_LEVEL_COURSES = [c for c in ALL_OLD_COURSES if isUpperLevelCourse(c)]

ALL_12_COURSES = [c for c in ALL_OLD_COURSES if is12(c)]

ALL_BUT_12_COURSES = ALL_LOWER_LEVEL_COURSES + [c for c in ALL_OLD_COURSES if isUpperLevelCourse(c) and not is12(c)]

ALL_STUDENT_COURSES = ALL_LOWER_LEVEL_COURSES + ALL_UPPER_LEVEL_COURSES

TARGET_COURSES = [c for c in ALL_OLD_COURSES if is11(c)]
#TARGET_COURSES = ALL_LOWER_LEVEL_COURSES + ALL_BUT_12_COURSES
######################################################################################

ARCHIVE_CATEGORY_REPRODUCTION_APPENDIX = '_' + ARCHIVE_CATEGORY_NAME.lower() + '_' + str(LAST_YEAR)
ARCHIVE_COURSE_REPRODUCTION_APPENDIX = '_' + ARCHIVE_CATEGORY_NAME.lower() + '_' + str(LAST_YEAR)

logger.job('Building category graph. This might take a while....')
CATEGORY_ROOT_NODE = categorytree.buildCategoryGraph(m, TARGET_COURSES).children[0]
logger.success('Category graph built')

logger.job('Creating archive...')
ARCHIVE_CATEGORY_ID = m.category_get_by_name_and_parent(ARCHIVE_CATEGORY_NAME, 0)['id']
logger.job('Archive category name: ' + str(LAST_YEAR))
ARCHIVE_YEAR_CATEGORY_ID = m.category_get_by_name_and_parent(str(LAST_YEAR), ARCHIVE_CATEGORY_ID)['id']

logger.job('Reproducing category root node')
ARCHIVE_ROOT_NODE = categorytree.reproduceCategoryGraph(m, CATEGORY_ROOT_NODE, ARCHIVE_YEAR_CATEGORY_ID, ARCHIVE_CATEGORY_REPRODUCTION_APPENDIX)


print('')
logger.heading('Beginning with archiving')

for course in TARGET_COURSES:
    print('removing all students from course ', course['shortname'])
    unenrolAllStudentsFromCourse(course['id'])
    print('archiving course ', course['shortname'])
    archiveCourse(course)

logger.done()

Hallo Michael,

ich dachte, mein Plugin läuft bei Dir? Das ignoriert, ob eine Globale Gruppe von einem anderen Plugin verwaltet wird. Und wenn das andere Plugin gut an wird sie anschließend gleich wieder neu angelegt.

Aber wenn Du die alten CSVs noch hast, dann ist das vermutlich auch damit schnell erledigt - einfach eine Spalte „löschen“ ergänzen und fertig.

Natürlich ist auch die REST-API eine gute Option. Du musst ja nicht Python nehmen, sowas geht in ganz vielen Sprachen.

Viele Grüße

Jörg

Hallo, das tut es.
Ich habe diese profilbasierten Einträge: „Wenn Eintrag 5a im Profil enthalten ist → gehöre automatisch zur globalen Gruppe 5a“. Das war sehr nervig für alle Gruppen einzeln anzulegen und ich habe einfach die Befürchtung, dass das nicht mehr läuft, wenn die globalen Gruppen für die Klassen (kurzzeitig) gelöscht werden :thinking: :interrobang:
Hast Du das so gemacht und alle globalen Klassengruppen mit Deinem Script gelöscht? Wurden sie danach zuverlässig wieder neu gefüllt?

Ich kann dank deines Plugins immerhin jetzt für alle Oberstufenkurse so vorgehen wie geplant. Das hat nun auch geklappt, so dass diese Kurse so ins Archiv gewandert sind, wie es sein soll: Es ist nur noch der alte Fachlehrer eingetragen, sämtliches eigenes Material ist noch da aber sämtliche Schülerabgaben und eingetragenen Schüler sind weg :slight_smile:

Evtl muss ich bei den paar Klassen dann einmal mit einem Upload einer neuen CSV-Datei klar kommen. Ganz klar ist das für die Klassen, die jetzt in Archiv stehen, noch nicht… aber ich bleibe dran
Viele Grüße
Michael

:roll_eyes: … ich hatte das alles schon mal so gemacht und völlig vergessen, dass ich die passende Vorlage sogar an passender Stelle abgespeichert hatte! Hier nochmal die Syntax der Kopfzeilen, die man benötigt, wenn man die globalen Gruppen als Einschreibemethode für diverse Kurse wieder deaktivieren will:

shortname;fullname;category;summary;role_editingteacher;role_teacher;role_student;enrolment_1;enrolment_2;enrolment_2_disable;enrolment_3;enrolment_3_delete;rename
5a;5a;459;Dein virtueller Klassenraum!;Lehrer/in;Lehrer/in;Schüler/in;manual;self;1;cohort;1;5a_Archiv_2021

Man gibt also nur die KategorieID (hier 459) an. Den Rest findet moodle dann anhand der Kursnamen (Lang- und Kurzname) eigenständig. Und hier nochmal ein Screenshot der Einstellungen unter:
Startseite -> Website-Administration -> Kurse -> Kursliste hochladen

So „einfach“ kann’s gehen :blush: … danach kann man auch wieder mit moosh und den gewohnten Befehlen zum Reset der Kurse weiterarbeiten …

Hallo Michael,

nun sitze ich selbst an der Sache und sehe, dass ich letztes Jahr das Plugin „Upload enrolment methods“ verwendet habe – auch eine gute Option. Es ist bei Belwü vorinstalliert und findet sich bei Plugins → Enrolments.

Beste Grüße

Jörg

Hi Jörg. Jau – kenne ich und ist hier auch installiert. Mittlerweile habe ich mit verschiedenen CSV-Files die komplette Struktur für die Unter- und Mittelstufe wieder angelegt. Die globalen Gruppen sind wieder da und die neuen Klassenlehrer sind schon mal eingeschrieben. Alle Fachlehrer folgen erst, wenn der Stundenplan klar ist. Aber der Anfang ist gemacht – und dieses Mal auch besser dokumentiert als vorher.

Viele Grüße,
Michael

… und noch eine Rückmeldung … gestern habe ich mit Hilfe des neuen Stundenplans die neuen Fachlehrer erzeugen lassen und habe damit alle neuen globalen Gruppen erzeugt und eingeschrieben.

Da sich ein Fehler meinerseits eingeschlichen hatte, wurden im ersten Durchgang völlig falsche Gruppen zusammengestellt und in die Klassen eingeschrieben.

So kam zum zweiten Mal Dein Plugin zum Einsatz und ich konnte in sehr kurzer Zeit alles wieder rückgängig machen, was da schief gelaufen ist. Das Bemerkenswerte daran war, dass dieses Mal nicht nur die globalen Gruppen entfernt wurden sondern auch gleich wieder aus den Kursen ausgeschrieben wurden.
Es macht also „moodle-intern“ einen Unterschied, ob man die User zunächst manuell einschreibt und erst dann zu globalen Gruppen hinzufügt – oder ob man sie direkt als globale Gruppe einschreibt und Kursen hinzufügt.
So war das Problem jedenfalls schnell behoben.

Die Kursstruktur für die Unter-/Mittelstufe ist jetzt vollständig vorhanden: Alle Klassen-/Fachlehrer sind eingeschrieben und überall sind die richtigen Schüler drin. Natürlich wurden auch gleich Gruppen pro Kurs mit angelegt … soweit so gut.

Viele Grüße,
Michael