diff --git a/abgabesystem.py b/abgabesystem.py new file mode 100644 index 0000000..710beca --- /dev/null +++ b/abgabesystem.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 + +import argparse +import yaml +import gitlab +import datetime +import logging as log +import csv +import secrets + + +class Deadline(yaml.YAMLObject): + """A deadline""" + + yaml_tag = 'Deadline' + + def __init__(self, tag, time, ref): + self.tag = tag + self.time = time + self.ref = ref + + def tag(self, project): + """Create protected tag on ref for all deadlines that have been + reached""" + + try: + project.tags.create({ + 'tag_name': self.tag, + 'ref': self.ref + }) + except gitlab.exceptions.GitlabHttpError as e: + log.warn(e) + + +class Course(yaml.YAMLObject): + """A course""" + + yaml_tag = 'Course' + + def setup_gitlab_group(self, groups): + try: + found = groups.list(search=self.name) + return found[0] + except gitlab.exceptions.GitlabHttpError as e: + log.info(e) + if e.response_code == 404: + gl.groups.create({}) + path = self.name.replace(' ', '_').lower() + group = gl.groups.create({ + 'name': self.name, + 'path': path + }) + for repo in self.repos: + group.repos.create({ + 'name': repo, + 'namespace_id': group.path, + 'visibility': 'internal' + }) + return group + else: + raise e + + def __init__(self, name, deadlines, repos): + self.name = name + self.repos = repos + self.deadlines = deadlines + + def projects(self, groups): + return groups.list(search=self.name)[0].projects.list() + + +class Student(): + """A student""" + + def __init__(self, user, mail, name, group): + self.user = user + self.email = mail + self.name = name + self.group = group + + def setup_project(self, base, course): + """Create user projects as forks from course/solutions in namespace of + course and add user as developer (NOT master) user should not be able + to modify protected TAG or force-push on protected branch users can + later invite other users into their projects""" + + fork = course.group.projects.list(name=self.user) + if len(fork) == 0: + fork = base.forks.create({ + 'name': self.user, + 'namespace': base.namspace, + 'visibility': 'private' + }) + + fork.members.create({ + 'user_id': self.user, + 'access_level': gitlab.DEVELOPER_ACCESS + }) + + return fork + + def setup_ldap_dummy(self, users, provider, basedn): + # TODO call from creation of student object() + + """Creates a dummy user for users that do not exist in gitlab + but in LDAP and have not logged in yet""" + + users = users.list(search=self.user) + + if len(users) == 0: + dummy = users.create({ + 'email': self.email, + 'username': self.name, + 'name': self.user, + 'provider': provider, + 'skip_confirmation': True, + 'extern_uid': 'uid=%s,%s' % (self.user, basedn), + 'password': secrets.token_urlsafe(nbytes=32) + }) + + return dummy + else: + return users[0] + + def from_csv(csvfile): + reader = csv.DictReader(csvfile, delimiter=';', quotechar='"') + + for line in reader: + student = Student(line['Nutzernamen'], + line['E-Mail'], + line['Vorname'] + ' ' + line['Nachname'], + line['Gruppe']) + yield student + + +def create_students(gl, conf, args): + with open(args.students[0], 'r') as csvfile: + for student in Student.from_csv(csvfile): + student.setup_ldap_dummy(gl.users, conf['ldap']['provider'], conf['ldap']['basedn']) + student.setup_project('solutions', args['course'][0]) #TODO + print(student) + + +def create_courses(gl, conf, args): + for course in conf['courses']: + course.setup_gitlab_group(gl.groups) + print(course) + + +def create_deadlines(gl, conf, args): + for course in conf['courses']: + for deadline in course.deadlines: + pass + #if deadline.time <= datetime.datetime.now(): + # deadline has approached + # TODO setup cronjob? + # deadline.tag(project) + + +if __name__ == '__main__': + + gl = gitlab.Gitlab.from_config() + gl.auth() + + parser = argparse.ArgumentParser() + parser.add_argument('--config', type=str, nargs=1, help='path to config file', default='config.yml') + + course_parser = parser.add_subparsers('courses') + course_parser.set_defaults(func=create_courses) + + student_parser = parser.add_subparsers('students') + student_parser.add_argument('students', desc='Exported CSV file from Stud.IP', nargs=1, type=str) + student_parser.add_argument('course', desc='Course for CSV file', nargs=1, type=str) + student_parser.set_defaults(func=create_students) + + deadline_parser = parser.add_subparsers('deadlines') + deadline_parser.set_defaults(func=create_deadlines) + + args = parser.parse_args() + + with open(args.config, 'r') as conf: + conf = yaml.safe_load(conf) + args.func(gl, conf, args) diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..fe2436f --- /dev/null +++ b/config.yml @@ -0,0 +1,23 @@ +ldap: + basedn: "ou=people,dc=tu-bs,dc=de" + dummy_user: + provider: main + skip_confirmation: true + +courses: + - +--- !Course + name: Programmieren 1 + repos: [solutions, exercises] + deadlines: + - + --- !Deadline + tag: Exercise 1 + time: 2018-04-01t00:00.00 + ref: master + + - + --- !Deadline + tag: Exercise 2 + time: 2018-04-15t15:00.00 + ref: master diff --git a/groupimporter.py b/groupimporter.py deleted file mode 100644 index ae10e44..0000000 --- a/groupimporter.py +++ /dev/null @@ -1,189 +0,0 @@ -import csv -import argparse -import gitlab - -from mimetypes import guess_type - - -DEFAULT_REPOS = ['solutions', 'exercises'] -BASEDN = 'ou=people,dc=tu-bs,dc=de' - -gl = gitlab.Gitlab.from_config() -gl.auth() - - -def group_name(longname): - """Use the group number (located at start of group name)""" - - if longname in ['keine Teilnahme an den Übungen', 'keiner Funktion oder Gruppe zugeordnet', 'Gruppe']: - return None - else: - return longname.split(' ')[0] - - -def parse_groups_csv(csvfile, encoding='utf-8'): - """Reads the names of the groups from CSV file and yields the group names - """ - - with open(csvfile, 'r', encoding=encoding) as lines: - """ Get distinct values from first column (groups)""" - - reader = csv.reader(lines, delimiter=';', quotechar='"') - zipped = list(zip(*reader)) - - for groupname in set(zipped[0]): - short_name = group_name(groupname) - - """Discard groups that are None""" - if short_name: - yield(short_name) - - -def parse_users_csv(csvfile, encoding='utf-8'): - """Reads user information from a CSV file and yields id and group""" - - with open(csvfile, 'r', encoding=encoding) as lines: - reader = csv.DictReader(lines, delimiter=';', quotechar='"') - - for line in reader: - user = object() - user.id = line['Nutzernamen'] - user.email = line['E-Mail'] - user.name = line['Vorname'] + ' ' + line['Nachname'] - user.group = group_name(line['Gruppe']) - - -def create_student(user_id, email, tutorial): - """Create user and add to tutorial group and workgroup based on preferred Abgabepartner - User will later be updated with password from LDAP - - https://gitlab.com/gitlab-org/gitlab-ee/issues/699 - """ - pass - - -def create_abgabegruppe(tutorial): - """Create a Abgabegruppe""" - pass - - -def create_tutorial(course, group): - """Creates a group via Gitlab API""" - - # this is the short path, not the full path - path = group.replace(' ', '_').lower() - - try: - subgroup = gl.groups.create({'name': group, 'path': path, 'parent_id': course.id}) - return subgroup - except gitlab.exceptions.GitlabHttpError as e: - print(e) - - -def create_course(course_name): - - courses = gl.groups.list(search=course_name) - course = None - - if len(courses) == 0: - path = course_name.replace(' ', '_').lower() - course = gl.groups.create({'name': course_name, 'path': path}) - else: - course = courses[0] - - return course - - -def create_course_project(name, course): - - try: - gl.projects.create({ - 'name': name, - 'namespace_id': course.id, - 'visibility': 'internal' - }) - except gitlab.exceptions.GitlabHttpError as e: - print(e) - - -def setup_user_project(user, base, course): - """Create user projects as forks from course/solutions - in namespace of course and add user as developer (NOT master) - user should not be able to modify protected TAG or force-push on protected branch - users can later invite other users into their projects""" - - fork = course.projects.list(name=user) - if len(fork) == 0: - fork = base.forks.create({ - 'name': user.id, - 'namespace': base.namspace, - 'visibility': 'private' - }) - - fork.members.create({ - 'user_id': user.id, - 'access_level': gitlab.DEVELOPER_ACCESS - }) - - return fork - - -def create_dummy_ldap_user(user): - """Creates a dummy user for users that do not exist in gitlab - but in LDAP and have not logged in yet""" - - users = gl.users.list(search=user) - - if len(users) == 0: - dummy = gl.users.create({ - 'email': user.email, - 'username': user.name, - 'name': user.id, - 'provider': 'main', - 'skip_confirmation': true, - 'extern_uid': 'uid=%s,%s' % (user.id, basedn), - 'password': secrets.token_urlsafe(nbytes=32) - }) - - return dummy - else: - return users[0] - - -if __name__ == '__main__': - - parser = argparse.ArgumentParser(description='Import groups and users from data source') - parser.add_argument('course', type=str, nargs=1, help='name of the course') - parser.add_argument('source', type=str, nargs=1, help='data source') - parser.add_argument('--encoding', type=str, nargs=1, help='encoding of source') - - args = parser.parse_args() - - type, _ = guess_type(args.source[0]) - course = args.course[0] - - course = create_course(course) - - for repo in DEFAULT_REPOS: - course_project = create_course_project(repo, course) - - users = None - - if type == 'text/csv': - # TODO get functions from API and add hiwis to admins - users = parse_users_csv(args.source[0], args.encoding[0]) - - elif type == None: - - print('MIME type not recognized') - - for user in users: - create_dummy_user_ldap(user) - setup_user_project(user, course) - -"""TODO -- add users (LDAP) with custom attribute for group, matrikelnummer - + if user exists set custom attributes - -- create search for custom attribute (-> checkout repos for group) -"""