321 lines
10 KiB
Python
321 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import gitlab
|
|
import logging as log
|
|
import csv
|
|
import secrets
|
|
import subprocess
|
|
import os
|
|
|
|
|
|
class Student():
|
|
"""A Gitlab user
|
|
|
|
Students are read from the CSV file that was exported from Stud.IP.
|
|
For each user, a dummy LDAP user is created in Gitlab.
|
|
Upon the first login Gitlab fetches the complete user using LDAP.
|
|
"""
|
|
|
|
def __init__(self, user, mail, name, group):
|
|
self.user = user
|
|
self.email = mail
|
|
self.name = name
|
|
self.group = group
|
|
|
|
def from_csv(csvfile):
|
|
"""Creates an iterable containing the users"""
|
|
reader = csv.DictReader(csvfile, delimiter=';', quotechar='"')
|
|
|
|
for line in reader:
|
|
yield Student(line['Nutzernamen'], line['E-Mail'], line['Vorname']
|
|
+ ' ' + line['Nachname'], line['Gruppe'])
|
|
|
|
|
|
def create_tag(project, tag, ref):
|
|
"""Creates protected tag on ref
|
|
|
|
The tag is used by the abgabesystem to mark the state of a solution at the
|
|
deadline
|
|
"""
|
|
|
|
print('Project %s. Creating tag %s' % (project.path, tag))
|
|
|
|
project.tags.create({
|
|
'tag_name': tag,
|
|
'ref': ref
|
|
})
|
|
|
|
|
|
def get_students(gl, students_csv):
|
|
"""Returns already existing GitLab users for students from provided CSV file that have an account.
|
|
"""
|
|
|
|
for student in Student.from_csv(students_csv):
|
|
users = gl.users.list(search=student.user)
|
|
if len(users) > 0:
|
|
yield users[0]
|
|
|
|
|
|
def create_user(gl, student, ldap_base, ldap_provider):
|
|
"""Creates a GitLab user account student.
|
|
Requires admin privileges.
|
|
"""
|
|
|
|
user = gl.users.create({
|
|
'email': student.email,
|
|
'username': student.user,
|
|
'name': student.name,
|
|
'provider': ldap_provider,
|
|
'skip_confirmation': True,
|
|
'extern_uid': 'uid=%s,%s' % (student.user, ldap_base),
|
|
'password': secrets.token_urlsafe(nbytes=32)
|
|
})
|
|
user.customattributes.set('group', student.group)
|
|
|
|
return user
|
|
|
|
|
|
def create_users(gl, args):
|
|
"""Creates Gitlab users from exported students list
|
|
"""
|
|
with open(args.students, encoding='iso8859') as students_csv:
|
|
for student in Student.from_csv(students_csv):
|
|
try:
|
|
create_user(gl, student, args.ldap_base, args.ldap_provider)
|
|
except gitlab.exceptions.GitlabCreateError:
|
|
log.warn('Failed to create user: %s' % student.user)
|
|
|
|
|
|
def fork_reference(gl, reference, namespace, deploy_key):
|
|
"""Create fork of solutions for student.
|
|
"""
|
|
|
|
fork = reference.forks.create({
|
|
'namespace': namespace.id
|
|
})
|
|
project = gl.projects.get(fork.id)
|
|
project.visibility = 'private'
|
|
project.container_registry_enabled = False
|
|
project.lfs_enabled = False
|
|
deploy_key = project.keys.create({
|
|
'title': "Deploy Key",
|
|
'key': deploy_key
|
|
})
|
|
project.keys.enable(deploy_key.id)
|
|
project.save()
|
|
|
|
return project
|
|
|
|
|
|
def create_project(gl, group, user, reference, deploy_key):
|
|
"""Creates a namespace (subgroup) and forks the project with
|
|
the reference solutions into that namespace
|
|
"""
|
|
|
|
subgroup = None
|
|
|
|
try:
|
|
subgroup = gl.groups.create({
|
|
'name': user.username,
|
|
'path': user.username,
|
|
'parent_id': group.id
|
|
})
|
|
except gitlab.exceptions.GitlabError as e:
|
|
subgroups = group.subgroups.list(search=user.username)
|
|
if len(subgroups) > 0 and subgroup[0].name == user.username:
|
|
subgroup = subgroups[0]
|
|
subgroup = gl.groups.get(subgroup.id, lazy=True)
|
|
else:
|
|
raise(e)
|
|
try:
|
|
subgroup.members.create({
|
|
'user_id': user.id,
|
|
'access_level': gitlab.DEVELOPER_ACCESS,
|
|
})
|
|
except gitlab.exceptions.GitlabError:
|
|
log.warning('Failed to add student %s to its own group' % user.username)
|
|
|
|
try:
|
|
fork_reference(gl, reference, subgroup, deploy_key)
|
|
except gitlab.exceptions.GitlabCreateError as e:
|
|
log.warning(e.error_message)
|
|
|
|
|
|
def setup_course(gl, group, students_csv, deploy_key):
|
|
"""Sets up the internal structure for the group for use with the course
|
|
"""
|
|
solution = None
|
|
reference_project = None
|
|
|
|
try:
|
|
solution = gl.groups.create({
|
|
'name': 'solutions',
|
|
'path': 'solutions',
|
|
'parent_id': group.id,
|
|
'visibility': 'internal',
|
|
})
|
|
except gitlab.exceptions.GitlabCreateError as e:
|
|
log.info('Failed to create solutions group. %s' % e.error_message)
|
|
solutions = group.subgroups.list(search='solutions')
|
|
if len(solutions) > 0 and solutions[0].name == 'solutions':
|
|
solution = gl.groups.get(solutions[0].id, lazy=True)
|
|
else:
|
|
raise(gitlab.exceptions.GitlabCreateError(error_message='Failed to setup solutions subgroup'))
|
|
|
|
try:
|
|
reference_project = gl.projects.create({
|
|
'name': 'solutions',
|
|
'namespace_id': solution.id,
|
|
'visibility': 'internal',
|
|
})
|
|
reference_project.commits.create({
|
|
'branch': 'master',
|
|
'commit_message': 'Initial commit',
|
|
'actions': [
|
|
{
|
|
'action': 'create',
|
|
'file_path': 'README.md',
|
|
'content': 'Example solutions go here',
|
|
},
|
|
]
|
|
})
|
|
except gitlab.exceptions.GitlabCreateError as e:
|
|
log.info('Failed to setup group structure. %s' % e.error_message)
|
|
projects = solution.projects.list(search='solutions')
|
|
if len(projects) > 0 and projects[0].name == 'solutions':
|
|
reference_project = gl.projects.get(projects[0].id)
|
|
else:
|
|
raise(gitlab.exceptions.GitlabCreateError(error_message='Failed to setup reference solutions'))
|
|
|
|
if solution is None or reference_project is None:
|
|
raise(gitlab.exceptions.GitlabCreateError(error_message='Failed to setup course'))
|
|
|
|
for user in get_students(gl, students_csv):
|
|
create_project(gl, solution, user, reference_project, deploy_key)
|
|
|
|
|
|
def projects(gl, args):
|
|
"""Creates the projects for all course participants
|
|
"""
|
|
groups = gl.groups.list(search=args.course)
|
|
if len(groups) == 0 and groups[0].name == args.course:
|
|
log.warn('This group does not exist')
|
|
else:
|
|
group = groups[0]
|
|
with open(args.deploy_key, 'r') as key, open(args.students, encoding='iso8859') as students_csv:
|
|
key = key.read()
|
|
setup_course(gl, group, students_csv, key)
|
|
|
|
|
|
def deadline(gl, args):
|
|
"""Checks deadlines for course and triggers deadline if it is reached"""
|
|
|
|
deadline_name = args.tag_name
|
|
try:
|
|
reference = gl.projects.get(args.reference, lazy=True)
|
|
|
|
for fork in reference.forks.list():
|
|
project = gl.projects.get(fork.id, lazy=False)
|
|
try:
|
|
create_tag(project, deadline_name, 'master')
|
|
except gitlab.exceptions.GitlabCreateError as e:
|
|
print(e.error_message)
|
|
|
|
except gitlab.exceptions.GitlabGetError as e:
|
|
print(e.error_message)
|
|
|
|
|
|
def plagiates(gl, args):
|
|
"""Runs the plagiarism checker (JPlag) for the solutions with a certain tag
|
|
"""
|
|
|
|
tag = args.tag_name
|
|
reference = gl.projects.get(args.reference, lazy=True)
|
|
try:
|
|
os.mkdir('solutions')
|
|
except os.FileExistsError as e:
|
|
print(e)
|
|
os.chdir('solutions')
|
|
|
|
for fork in reference.forks.list():
|
|
project = gl.projects.get(fork.id, lazy=True)
|
|
try:
|
|
subprocess.run(
|
|
['git', 'clone', '--branch', tag, project.ssh_url_to_repo, project.path_with_namespace])
|
|
os.chdir('..')
|
|
except:
|
|
print(e.error_message)
|
|
|
|
subprocess.run(
|
|
['java', '-jar', args.jplag_jar, '-s', 'solutions', '-p', 'java', '-r', 'results', '-bc', args.reference, '-l', 'java17'])
|
|
|
|
|
|
def course(gl, args):
|
|
"""Creates the group for the course
|
|
"""
|
|
try:
|
|
group = gl.groups.create({
|
|
'name': args.course,
|
|
'path': args.course,
|
|
'visibility': 'internal',
|
|
})
|
|
log.info('Created group %s' % args.course)
|
|
except gitlab.exceptions.GitlabCreateError as e:
|
|
log.warning('Failed to create group %s. %s' % (args.course, e.error_message))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
gl = gitlab.Gitlab.from_config()
|
|
gl.auth()
|
|
log.info('authenticated')
|
|
|
|
parser = argparse.ArgumentParser()
|
|
subparsers = parser.add_subparsers(title='subcommands')
|
|
|
|
user_parser = subparsers.add_parser(
|
|
'users',
|
|
help='Creates users from LDAP')
|
|
user_parser.set_defaults(func=create_users)
|
|
user_parser.add_argument('-s', '--students', dest='students')
|
|
user_parser.add_argument('-b', '--ldap-base', dest='ldap_base')
|
|
user_parser.add_argument('-p', '--ldap-provider', dest='ldap_provider')
|
|
|
|
course_parser = subparsers.add_parser(
|
|
'courses',
|
|
help='Creates a new course')
|
|
course_parser.set_defaults(func=course)
|
|
course_parser.add_argument('-c', '--course', dest='course')
|
|
|
|
projects_parser = subparsers.add_parser(
|
|
'projects',
|
|
help='Sets up the projects and groups for a course')
|
|
projects_parser.set_defaults(func=projects)
|
|
projects_parser.add_argument('-c', '--course', dest='course')
|
|
projects_parser.add_argument('-d', '--deploy-key', dest='deploy_key')
|
|
projects_parser.add_argument('-s', '--students', dest='students')
|
|
|
|
deadline_parser = subparsers.add_parser(
|
|
'deadline',
|
|
description='Sets the tags at a deadline to permanently mark it in the version history')
|
|
deadline_parser.set_defaults(func=deadline)
|
|
deadline_parser.add_argument('-t', '--tag-name', dest='tag_name')
|
|
deadline_parser.add_argument('-r', '--reference', dest='reference')
|
|
|
|
plagiates_parser = subparsers.add_parser(
|
|
'plagiates',
|
|
description='Runs the plagiarism checker on all solutions using a reference project as the baseline')
|
|
plagiates_parser.set_defaults(func=plagiates)
|
|
plagiates_parser.add_argument('-t', '--tag-name', dest='tag_name')
|
|
plagiates_parser.add_argument('-r', '--reference', dest='reference')
|
|
plagiates_parser.add_argument('-j', '--jplag-jar', dest='jplag_jar')
|
|
|
|
args = parser.parse_args()
|
|
|
|
log.basicConfig(filename='example.log', filemode='w', level=log.DEBUG)
|
|
|
|
if 'func' in args:
|
|
args.func(gl, args)
|
|
else:
|
|
parser.print_help()
|