Requirement specification

  • A  new student import deletes old school classes and student accounts that are no longer needed but leaves still existing ones in place.
    (The simple deletion of all school classes and student accounts can be done by importing an empty CSV file.)
  • The CSV file to be imported can be passed as a command line parameter.

Download, installation and usage

The software solution is versioned on Github.com and can be downloaded: 

https://github.com/ateachment/AD-StudentManagement

Installation and user instructions can be found further down on this page.


Main program 'importStudents.py'

The file importStudents.py contains two functions each for deleting school classes and students that have become obsolete. Thereby deleteObsoleteStudentsFromAD() is called from deleteObsoleteSchoolClassesFromAD().

Further down, in the main programme, the CSV file to be imported is either specified as a command line parameter or entered in the programme itself.

 1  
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from School import *                            # import classes
from User import *
from Csv import Csv 
from Ldap import PyAD
import sys


def deleteObsoleteSchoolClassesFromAD(school):
    ldap = PyAD(Settings.dnStudents)
    ldapClassNames = ldap.getSchoolClasses()
    classNames = []
    for schoolClass in school.getSchoolClasses():
        classNames.append(schoolClass.getName())
    for ldapClassName in ldapClassNames:
        if ldapClassName not in classNames:     # school class in ldap is obsolete
            ldap.deleteSchoolClass(ldapClassName)
        else:
            deleteObsoleteStudentsFromAD(school, ldapClassName)

def deleteObsoleteStudentsFromAD(school, className):
    ldap = PyAD(Settings.dnStudents)
    ldapStudents = ldap.getStudents(className)
    studentsNames = []
    for schoolClass in school.getSchoolClasses():
        if schoolClass.getName() == className:
            students = schoolClass.getStudents()
            for student in students:
                studentsNames.append([student.getSurname(), student.getFirstname(), student.getDateOfBirth()])
    for ldapStudent in ldapStudents:
        if ldapStudent not in studentsNames:
            ldap.deleteStudent(ldapStudent[0], ldapStudent[1], ldapStudent[2], className)



if __name__ == "__main__":
    if(len(sys.argv)) == 1:                 # no command line argument
        filename = input("CSV import file with students: ")
    else:                                   # filename as argument
        filename = sys.argv[1]
        
    school = School([])
    print(filename + " is being imported ...")
    importStudents = school.importStudentsFromCSV(filename)      # import students
    print("Delete obsolete school classes ...")
    deleteObsoleteSchoolClassesFromAD(school)
    print("Delete obsolete student accounts from the Active Directory and add new ones ...")
    school.addToLDAP()
    print("Done!")

Main program 'importStudents.py'


ldap.py

The class ldap has received 3 more methods getSchoolClasses(), getStudents() and findStudent(). They are used to find school classes and students that already exist in AD. In order to remove or preserve them if necessary:

  1  
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
from abc import ABC, abstractmethod     # import Abstract Base Classes (ABCs)

class AbstractLDAP(ABC):
 
    def __init__(self, dnStudents):
        self._dnStudents = dnStudents
        super().__init__()
    
    @abstractmethod
    def createStudent(self, username, nameSchoolClass):
        pass
    @abstractmethod
    def deleteStudent(self, username, nameSchoolClass):
        pass
    @abstractmethod
    def createSchoolClass(self, nameSchoolClass):
        pass
    @abstractmethod
    def deleteSchoolClass(self, nameSchoolClass):
        pass

from pyad import *
from Settings import Settings
from Fileserver import *

class PyAD(AbstractLDAP):
    def createStudent(self, username, surname, firstname, password, dateOfBirth, nameSchoolClass):
        query = adquery.ADQuery()
        cnUnique = False
        numChar = 0
        while(cnUnique == False):
            query.execute_query(attributes = ["cn"],            # query: does cn already exist
                where_clause=("cn = '" + username + "'"),
                base_dn = Settings.dnSchool)
            if query.get_row_count() > 0:                       # yes: add character
                numChar = numChar + 1
                if(numChar <= len(firstname)):                  # if the character set of the first name is large enough
                    username = surname + firstname[:numChar]    # add number of characters from beginning of first ame
                else:                                           # if the character set of the first name is used up
                    username = surname + firstname + (numChar - len(firstname))  * 'x'  # added number of 'x'
            else:                                               # cn is unique => create user
                cnUnique = True
                ouSchoolClass = pyad.adcontainer.ADContainer("ou="+nameSchoolClass+", "+self._dnStudents, adsi_ldap_com_object=None, options={})
                student = aduser.ADUser.create(username
                                     , ouSchoolClass
                                     , password=password
                                     , upn_suffix=None
                                     , enable=True
                                     , optional_attributes={"sn":surname
                                                            , "givenName":firstname
                                                            , "employeeID":dateOfBirth  # using existing employeeId attribute is easier than extending AD scheme
                                                            , "description":"Student"
                                                            , "pwdLastSet":0            # password must be changed on first login
                                                            , "homeDrive":Settings.homeDrive                                   # set home drive
                                                            , "homeDirectory":Settings.shareHomeStudents + "\\" + username})   # set home directory
                # put new student in global group of its school class
                gl_groupSchoolClass = pyad.from_dn("cn=GL_" + nameSchoolClass + ", " + Settings.dnGLGroups)   
                gl_groupSchoolClass.add_members([student])           
        return username    # return possibly modified username     

    def deleteStudent(self, surname, firstname, dateOfBirth, nameSchoolClass):
        # print("delete ", surname, firstname, dateOfBirth, nameSchoolClass)
        dnSchoolClass = "ou="+nameSchoolClass + ", " + self._dnStudents
        query = adquery.ADQuery()
        query.execute_query(attributes = ["cn"],
                where_clause=("sn = '" + surname + "' and givenName = '" + firstname + "' and employeeID = '" + dateOfBirth + "'"),
                base_dn = dnSchoolClass)
        if query.get_row_count() >= 1:                              # if student exists => delete it
            ouSchoolClass = pyad.adcontainer.ADContainer(dnSchoolClass, adsi_ldap_com_object=None, options={})
            fs = Fileserver()
            for row in query.get_results():
                ouSchoolClass.from_cn(row["cn"]).delete()
                fs.deleteHomeDirStudent(row["cn"])
   
    def createSchoolClass(self, nameSchoolClass):
        query = adquery.ADQuery()
        query.execute_query(attributes = ["distinguishedName"],
                where_clause=("ou = '" +nameSchoolClass + "'"),
                base_dn = self._dnStudents)
        if query.get_row_count() == 0:                              # if school class not yet exists => create
            ouStudents = pyad.adcontainer.ADContainer(self._dnStudents, adsi_ldap_com_object=None, options={})
            ouStudents.create_container(nameSchoolClass)
            # create a new global group for students of school class
            ouGLgroups = pyad.adcontainer.ADContainer.from_dn(Settings.dnGLGroups)
            gl_groupSchoolClass = ouGLgroups.create_group("GL_" + nameSchoolClass
                                                 , security_enabled=True
                                                 , scope='GLOBAL'
                                                 , optional_attributes = {"description":"Students of school class " + nameSchoolClass})
            # create a new domaine local for project dir of students
            ouDLgroups = pyad.adcontainer.ADContainer.from_dn(Settings.dnDLGroups)
            dl_groupSchoolClass = ouDLgroups.create_group("DL_" + nameSchoolClass + "_F" 
                                            , security_enabled=True
                                            , scope='LOCAL'
                                            , optional_attributes = {"description":"Full access for project dir of students of school class " + nameSchoolClass})
            
            gl_groupStudents = pyad.from_dn("cn=GL_Students, " + Settings.dnGLGroups) # put global group of school class
            gl_groupStudents.add_members([gl_groupSchoolClass])             # into global group of all students "GL_Students" and
            dl_groupSchoolClass.add_members([gl_groupSchoolClass])          # into domaine local group of students of a school class
            # create folder and share
            fs = Fileserver()
            fs.createProjectDirStudent(nameSchoolClass)
            fs.addShareProjectDirStudent(nameSchoolClass)
 
    def deleteSchoolClass(self, nameSchoolClass):
        dnSchoolClass = "ou="+nameSchoolClass+", "+ self._dnStudents
        query = adquery.ADQuery()
        query.execute_query(attributes = ["distinguishedName"],
                where_clause=("ou = '" +nameSchoolClass + "'"),
                base_dn = self._dnStudents)
        if query.get_row_count() == 1:                          # if school class exists
            query = adquery.ADQuery()
            query.execute_query(attributes = ["sn", "givenName", "employeeID"],  # employeeID holds date of birth
                where_clause=("cn = '*'"),
                base_dn = dnSchoolClass)
            if query.get_row_count() > 0:                       # if school class not empty 
                for row in query.get_results():                 # delete students
                    self.deleteStudent(row["sn"], row["givenName"], row["employeeID"],  nameSchoolClass)
            ouSchoolClass = pyad.adcontainer.ADContainer(dnSchoolClass, adsi_ldap_com_object=None, options={})
            ouSchoolClass.delete()                              # delete school class ou
            # delete global and domaine local groups of school class
            gl_groupSchoolClass = pyad.from_dn("cn=GL_" + nameSchoolClass + ", " + Settings.dnGLGroups) 
            gl_groupSchoolClass.delete()
            dl_groupSchoolClass = pyad.from_dn("cn=DL_" + nameSchoolClass + "_F" + ", " + Settings.dnDLGroups) 
            dl_groupSchoolClass.delete()
            # delete folder and share
            fs = Fileserver()
            fs.deleteProjectDirStudent(nameSchoolClass)
            fs.deleteShareProjectDirStudent(nameSchoolClass)
            
    def getSchoolClasses(self):
        query = adquery.ADQuery()
        query.execute_query(attributes = ["ou"],
            where_clause=("ou = '*' and ou <> 'Students'"),     # exclude users and base dn
            base_dn = self._dnStudents)
        schoolClasses = []
        result = query.get_results()        
        for schoolClass in result:
            schoolClasses.append(schoolClass['ou'][0])
        return schoolClasses
    
    def getStudents(self, nameSchoolClass):
        dnSchoolClass = "ou="+nameSchoolClass + ", " + self._dnStudents
        query = adquery.ADQuery()
        query.execute_query(attributes = ['sn', 'givenName', 'employeeID'],
                    # surname, firstname, employeeID holds date of birth       
        # query.execute_query(attributes = ['cn', 'sn', 'givenName', 'employeeID'],  
                    # username, surname, firstname, employeeID holds date of birth       
            where_clause=("ou <> '" + nameSchoolClass + "'"),     # exclude base dn   
            base_dn = dnSchoolClass)
        students = []
        result = query.get_results()       
        for student in result:
            # students.append(student['cn'], [student['sn'], student['givenName'], student['employeeID']])
            students.append([student['sn'], student['givenName'], student['employeeID']])
        return students
    
    def findStudent(self, surname, firstname, dateOfBirth):
        # print("find ", surname, firstname, dateOfBirth)
        query = adquery.ADQuery()
        query.execute_query(attributes = ["distinguishedName"],
                where_clause=("sn = '" + surname + "' and givenName = '" + firstname + "' and employeeID = '" + dateOfBirth + "'"),
                base_dn = Settings.dnStudents)
        if query.get_row_count() >= 1:                              
            for row in query.get_results():
                # i.e. row["distinguishedName"] = CN=Colligan,OU=c1,OU=Students,OU=SchoolUsers,DC=trainingX,DC=net
                className = row["distinguishedName"].split(",")[1].split("=")[1]
                return className
        return ""   # not found -> not already in other school class

ldap.py


User.py

The addToLDAP() method has been improved here. Depending on whether and where a student already exists, different measures are taken:

If the student ...

  • already exists in its correct school class in AD, nothing needs to be done.
  • does not yet exist anywhere in AD, it is simply created in its school class.
  • already exists in another school class, it is simply deleted for the sake of simplicity and then recreated in its new school class. (A move to the new school class with all its data would be an improvement for a future version of the software).
 1  
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from Settings import Settings
from Ldap import PyAD
from Fileserver import *

class User:                                                 # superclass "User"
    def __init__(self, surname, firstname, dateOfBirth):    # constructor 
        self._surname = surname                             # protected properties/attributes => only one "_"
        self._firstname = firstname
        self._password = firstname[0] + surname[0] + dateOfBirth  
        self._dateOfBirth = dateOfBirth
        self._username = surname 
        
    def getSurname(self):                                  
        return self._surname
    
    def getFirstname(self):
        return self._firstname

    def getPassword(self):
        return self._password

    def getDateOfBirth(self):
        return self._dateOfBirth

    def getUsername(self):
        return self._username


class Student(User):    # subclass "Student" inherits every property and method from superclass "User"
    def __init__(self, surname, firstname, dateOfBirth, schoolClass):
        User.__init__(self, surname, firstname, dateOfBirth)  # constructor of superclass is invoked
        schoolClass.addStudent(self);   # adds himself to school class
        self.__schoolClass = schoolClass

    def getSchoolClass(self):              
        return self.__schoolClass

    def addToLDAP(self):
        ldap = PyAD(Settings.dnStudents)
        # add to ldap if not already in school class
        foundStudentSchoolClass = ldap.findStudent(self._surname , self._firstname, self._dateOfBirth)
        if foundStudentSchoolClass == self.__schoolClass.getName():
            pass # do nothing
        elif foundStudentSchoolClass == "": # not already in another school class -> create student
            # ldap.createStudent() returns the possibly modified username for updating
            self._username = ldap.createStudent(self._username
                               , self._surname
                               , self._firstname
                               , self._password
                               , self._dateOfBirth
                               , self.__schoolClass.getName())

            fs = Fileserver()
            fs.createHomeDirStudent(self._username)   
        else:   # already in another school class 
                # -> delete and recreate student for simplicity reason
            ldap = PyAD(Settings.dnStudents)
            ldap.deleteStudent(self._surname, self._firstname, self._dateOfBirth, foundStudentSchoolClass)
            fs = Fileserver()
            fs.deleteHomeDirStudent(self._username)
            self._username = ldap.createStudent(self._username
                               , self._surname
                               , self._firstname
                               , self._password
                               , self._dateOfBirth
                               , self.__schoolClass.getName())
    
    def deleteFromLDAP(self):
        ldap = PyAD(Settings.dnStudents)
        ldap.deleteStudent(self._surname, self._firstname, self._dateOfBirth, self.__schoolClass.getName())

        fs = Fileserver()
        fs.deleteHomeDirStudent(self._username)

User.py


Test program 'testImportStudents.py'

Automatically executed unit tests are used here, which test the methods of the classes for their expected return values.
Attention all existing school classes and students in the AD are deleted in the process. Therefore, so that the test programme is not executed by mistake, programme lines 2 and 3 must first be commented out. Only then is the file executable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# filename: testImportStudents.py 
CAUTION: Running the Unittest file will delete all school classes and students from AD. 
Do NOT run the tests in production mode.

from School import *                        
from User import *
from Fileserver import *
from Ldap import PyAD
import importStudents 

import unittest                                     # import unit tests


class TestImportStudents(unittest.TestCase):
    @classmethod
    def setUpClass(self):                           # is executed once for all tests    
        self.schoolClass1 = SchoolClass("c1",[])         
        self.schoolClass2 = SchoolClass("c2",[])

        self.student1 = Student("Miller","Sam","22.11.2004",self.schoolClass1)  
        self.student2 = Student("Smith","Jake","13.02.2003",self.schoolClass1)
        self.student3 = Student("Hampton","Oliver","11.01.2004",self.schoolClass2)

        self.school = School([self.schoolClass1, self.schoolClass2]) 
    
    def test_School(self):                      # test names must start with 'test_' otherwise will not executed
        schoolClasses = self.school.getSchoolClasses()
        self.assertEqual(len(schoolClasses), 2, 'Number of school classes wrong.')
        self.assertEqual(schoolClasses[0].getName(), "c1", '1st school class wrong.')
        self.assertEqual(schoolClasses[1].getName(), "c2", '2nd school class wrong.')
        students = self.schoolClass1.getStudents()
        self.assertEqual(len(students), 2, 'Number of students wrong.')
        self.assertEqual(students[0].getUsername(),'Miller', 'Students username wrong.')

    def test_AddToLDAP(self):
        # remove school classes and/or students if there are any left in AD
        ldap = PyAD(Settings.dnStudents) 
        classNames = ldap.getSchoolClasses()
        for className in classNames:
            # print("delete school class: " + className)
            ldap.deleteSchoolClass(className)
        
        self.school.addToLDAP()
        ldap = PyAD(Settings.dnStudents)
        classNames = ldap.getSchoolClasses()
        self.assertEqual(classNames, ['c1', 'c2'], 'From ldap returned school classes wrong.')
        students = ldap.getStudents('c1')
        self.assertEqual(students, [["Miller", "Sam", "22.11.2004"], ["Smith", "Jake", "13.02.2003"]], 'From ldap returned students wrong.')
        # clean up
        schoolClasses = self.school.getSchoolClasses()
        for schoolClass in schoolClasses:
            schoolClass.deleteFromLDAP()
        classNames = ldap.getSchoolClasses()
        self.assertEqual(classNames, [], 'Ldap returns still school classe(s). Still not empty.')

    def test_ImportCsvFile(self):
        # remove school classes and/or students if there are any left in AD
        ldap = PyAD(Settings.dnStudents) 
        classNames = ldap.getSchoolClasses()
        for className in classNames:
            ldap.deleteSchoolClass(className)
        
        school = School([])
        school.importStudentsFromCSV("import/students.csv")
        school.addToLDAP()

        classNames = ldap.getSchoolClasses()
        
        self.assertEqual(classNames, ['c1', 'c2'], 'From ldap returned school classes wrong.')
        students = ldap.getStudents('c1')
        self.assertEqual(students, [["Colligan", "Manda", "22.09.2004"], ["Thieme", "Madelaine", "02.12.2000"]], 'From ldap returned students wrong.')
        
        school2 = School([])
        school2.importStudentsFromCSV("import/students2.csv")
        importStudents.deleteObsoleteSchoolClassesFromAD(school2)  # 'c2' will be deleted

        classNames = ldap.getSchoolClasses()
        self.assertEqual(classNames, ['c1'], 'From ldap returned school classes wrong.')

        school2.addToLDAP()
        classNames = ldap.getSchoolClasses()
        self.assertEqual(classNames, ['c1', 'c3'], 'From ldap returned school classes wrong.')
        students = ldap.getStudents('c1')
        self.assertEqual(students, [["Colligan", "Manda", "22.09.2004"], ["Belafonte", "Harry", "01.03.1927"]], 'From ldap returned students wrong.')
        students = ldap.getStudents('c3')
        self.assertEqual(students, [["Tarleton","Vito","22.02.2004"]], 'From ldap returned students wrong.')

        # clean up
        classNames = ldap.getSchoolClasses()
        for className in classNames:
            ldap.deleteSchoolClass(className)

if __name__ == '__main__':
    unittest.main()

testImportStudents.py


All other programme files remain unchanged and are explained in sections above.