From 39fd326ac2788d182db20a93d40656c47c2202da Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 27 Dec 2014 18:26:52 +0100 Subject: [PATCH 1/5] allow generated password for new osusers - change osusers.admin.UserCreationForm to allow empty password input which triggers the creation of a new password --- docs/changelog.rst | 4 ++++ gnuviechadmin/osusers/admin.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ff8024b..149e45c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,10 @@ Changelog ========= +* :feature:`-` allow empty password input in + :py:class:`osusers.admin.UserCreationForm` to allow generated passwords for + new users + * :release:`0.3.0 <2014-12-27>` * :feature:`-` call create/delete mailbox tasks when saving/deleting mailboxes * :support:`-` use celery routers from gvacommon diff --git a/gnuviechadmin/osusers/admin.py b/gnuviechadmin/osusers/admin.py index a46dbc5..d2835e2 100644 --- a/gnuviechadmin/osusers/admin.py +++ b/gnuviechadmin/osusers/admin.py @@ -27,10 +27,14 @@ class UserCreationForm(forms.ModelForm): A form for creating system users. """ - password1 = forms.CharField(label=_('Password'), - widget=forms.PasswordInput) - password2 = forms.CharField(label=_('Password (again)'), - widget=forms.PasswordInput) + password1 = forms.CharField( + label=_('Password'), widget=forms.PasswordInput, + required=False, + ) + password2 = forms.CharField( + label=_('Password (again)'), widget=forms.PasswordInput, + required=False, + ) class Meta: model = User From 18e47d73b4f507a634b54331408628820e6036b3 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 27 Dec 2014 19:26:16 +0100 Subject: [PATCH 2/5] add customer field to osusers.models.User - allow association of os users to Django users (customers) - adapt admin forms - add migration --- docs/changelog.rst | 1 + gnuviechadmin/osusers/admin.py | 5 +++-- .../osusers/migrations/0003_user_customer.py | 22 +++++++++++++++++++ gnuviechadmin/osusers/models.py | 7 ++++-- 4 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 gnuviechadmin/osusers/migrations/0003_user_customer.py diff --git a/docs/changelog.rst b/docs/changelog.rst index 149e45c..acc838a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* :feature:`-` add a `customer` field to :py:class:`osusers.models.User` * :feature:`-` allow empty password input in :py:class:`osusers.admin.UserCreationForm` to allow generated passwords for new users diff --git a/gnuviechadmin/osusers/admin.py b/gnuviechadmin/osusers/admin.py index d2835e2..1025038 100644 --- a/gnuviechadmin/osusers/admin.py +++ b/gnuviechadmin/osusers/admin.py @@ -38,7 +38,7 @@ class UserCreationForm(forms.ModelForm): class Meta: model = User - fields = [] + fields = ['customer'] def clean_password2(self): """ @@ -57,6 +57,7 @@ class UserCreationForm(forms.ModelForm): """ user = User.objects.create_user( + customer=self.cleaned_data['customer'], password=self.cleaned_data['password1'], commit=commit) return user @@ -75,7 +76,7 @@ class UserAdmin(admin.ModelAdmin): add_fieldsets = ( (None, { 'classes': ('wide',), - 'fields': ('password1', 'password2')}), + 'fields': ('customer', 'password1', 'password2')}), ) def get_form(self, request, obj=None, **kwargs): diff --git a/gnuviechadmin/osusers/migrations/0003_user_customer.py b/gnuviechadmin/osusers/migrations/0003_user_customer.py new file mode 100644 index 0000000..b8043ef --- /dev/null +++ b/gnuviechadmin/osusers/migrations/0003_user_customer.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('osusers', '0002_auto_20141226_1456'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='customer', + field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + ] diff --git a/gnuviechadmin/osusers/models.py b/gnuviechadmin/osusers/models.py index 93a4c3d..1b0d567 100644 --- a/gnuviechadmin/osusers/models.py +++ b/gnuviechadmin/osusers/models.py @@ -104,7 +104,9 @@ class UserManager(models.Manager): return nextuser @transaction.atomic - def create_user(self, username=None, password=None, commit=False): + def create_user( + self, customer, username=None, password=None, commit=False + ): uid = self.get_next_uid() gid = Group.objects.get_next_gid() if username is None: @@ -114,7 +116,7 @@ class UserManager(models.Manager): homedir = os.path.join(settings.OSUSER_HOME_BASEPATH, username) group = Group.objects.create(groupname=username, gid=gid) user = self.create(username=username, group=group, uid=uid, - homedir=homedir, + homedir=homedir, customer=customer, shell=settings.OSUSER_DEFAULT_SHELL) user.set_password(password) if commit: @@ -132,6 +134,7 @@ class User(TimeStampedModel, models.Model): gecos = models.CharField(_('Gecos field'), max_length=128, blank=True) homedir = models.CharField(_('Home directory'), max_length=256) shell = models.CharField(_('Login shell'), max_length=64) + customer = models.ForeignKey(settings.AUTH_USER_MODEL) objects = UserManager() From 0df67e7154879aa786ae7fc1c8aa34712287d638 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 27 Dec 2014 22:44:27 +0100 Subject: [PATCH 3/5] document osusers code --- gnuviechadmin/osusers/__init__.py | 5 + gnuviechadmin/osusers/admin.py | 100 ++++++++++++++++- gnuviechadmin/osusers/apps.py | 17 +++ gnuviechadmin/osusers/models.py | 181 ++++++++++++++++++++++++++++-- gnuviechadmin/osusers/tasks.py | 154 +++++++++++++++++++++++-- 5 files changed, 430 insertions(+), 27 deletions(-) create mode 100644 gnuviechadmin/osusers/apps.py diff --git a/gnuviechadmin/osusers/__init__.py b/gnuviechadmin/osusers/__init__.py index e69de29..24380a5 100644 --- a/gnuviechadmin/osusers/__init__.py +++ b/gnuviechadmin/osusers/__init__.py @@ -0,0 +1,5 @@ +""" +This app is for managing operating system users and groups. + +""" +default_app_config = 'osusers.apps.OsusersAppConfig' diff --git a/gnuviechadmin/osusers/admin.py b/gnuviechadmin/osusers/admin.py index 1025038..3bb3579 100644 --- a/gnuviechadmin/osusers/admin.py +++ b/gnuviechadmin/osusers/admin.py @@ -1,3 +1,7 @@ +""" +This module contains the Django admin classes of the :py:mod:`osusers` app. + +""" from django import forms from django.utils.translation import ugettext as _ from django.contrib import admin @@ -10,13 +14,24 @@ from .models import ( ) PASSWORD_MISMATCH_ERROR = _("Passwords don't match") +""" +Error message for non matching passwords. +""" class AdditionalGroupInline(admin.TabularInline): + """ + Inline for :py:class:`osusers.models.AdditionalGroup` instances. + + """ model = AdditionalGroup class ShadowInline(admin.TabularInline): + """ + Inline for :py:class:`osusers.models.ShadowInline` instances. + + """ model = Shadow readonly_fields = ['passwd'] can_delete = False @@ -24,7 +39,8 @@ class ShadowInline(admin.TabularInline): class UserCreationForm(forms.ModelForm): """ - A form for creating system users. + A form for creating :py:class:`operating system users + `. """ password1 = forms.CharField( @@ -44,6 +60,9 @@ class UserCreationForm(forms.ModelForm): """ Check that the two password entries match. + :return: the validated password + :rtype: str or None + """ password1 = self.cleaned_data.get('password1') password2 = self.cleaned_data.get('password2') @@ -55,6 +74,10 @@ class UserCreationForm(forms.ModelForm): """ Save the provided password in hashed format. + :param boolean commit: whether to save the created user + :return: user instance + :rtype: :py:class:`osusers.models.User` + """ user = User.objects.create_user( customer=self.cleaned_data['customer'], @@ -65,10 +88,16 @@ class UserCreationForm(forms.ModelForm): """ No additional groups are created when this form is saved, so this method just does nothing. + """ class UserAdmin(admin.ModelAdmin): + """ + Admin class for working with :py:class:`operating system users + `. + + """ actions = ['perform_delete_selected'] add_form = UserCreationForm inlines = [AdditionalGroupInline, ShadowInline] @@ -83,6 +112,13 @@ class UserAdmin(admin.ModelAdmin): """ Use special form during user creation. + :param request: the current HTTP request + :param obj: either a :py:class:`User ` instance or + None for a new user + :param kwargs: keyword arguments to be passed to + :py:meth:`django.contrib.admin.ModelAdmin.get_form` + :return: form instance + """ defaults = {} if obj is None: @@ -94,16 +130,47 @@ class UserAdmin(admin.ModelAdmin): return super(UserAdmin, self).get_form(request, obj, **defaults) def get_readonly_fields(self, request, obj=None): + """ + Make sure that uid is not editable for existing users. + + :param request: the current HTTP request + :param obj: either a :py:class:`User ` instance or + None for a new user + :return: a list of fields + :rtype: list + + """ if obj: return ['uid'] return [] def perform_delete_selected(self, request, queryset): + """ + Action to delete a list of selected users. + + This action calls the delete method of each selected user in contrast + to the default `delete_selected`. + + :param request: the current HTTP request + :param queryset: Django ORM queryset representing the selected users + + """ for user in queryset.all(): user.delete() perform_delete_selected.short_description = _('Delete selected users') def get_actions(self, request): + """ + Get the available actions for users. + + This overrides the default behavior to remove the default + `delete_selected` action. + + :param request: the current HTTP request + :return: list of actions + :rtype: list + + """ actions = super(UserAdmin, self).get_actions(request) if 'delete_selected' in actions: del actions['delete_selected'] @@ -111,19 +178,40 @@ class UserAdmin(admin.ModelAdmin): class GroupAdmin(admin.ModelAdmin): + """ + Admin class for workint with :py:class:`operating system groups + `. + + """ actions = ['perform_delete_selected'] - def get_inline_instances(self, request, obj=None): - if obj is None: - return [] - return super(GroupAdmin, self).get_inline_instances(request, obj) - def perform_delete_selected(self, request, queryset): + """ + Action to delete a list of selected groups. + + This action calls the delete method of each selected group in contrast + to the default `delete_selected`. + + :param request: the current HTTP request + :param queryset: Django ORM queryset representing the selected groups + + """ for group in queryset.all(): group.delete() perform_delete_selected.short_description = _('Delete selected groups') def get_actions(self, request): + """ + Get the available actions for groups. + + This overrides the default behavior to remove the default + `delete_selected` action. + + :param request: the current HTTP request + :return: list of actions + :rtype: list + + """ actions = super(GroupAdmin, self).get_actions(request) if 'delete_selected' in actions: del actions['delete_selected'] diff --git a/gnuviechadmin/osusers/apps.py b/gnuviechadmin/osusers/apps.py new file mode 100644 index 0000000..4ca9b7f --- /dev/null +++ b/gnuviechadmin/osusers/apps.py @@ -0,0 +1,17 @@ +""" +This module contains the :py:class:`django.apps.AppConfig` instance for the +:py:module:`osusers` app. + +""" +from __future__ import unicode_literals +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class OsusersAppConfig(AppConfig): + """ + AppConfig for the :py:mod:`osusers` app. + + """ + name = 'osusers' + verbose_name = _('Operating System Users and Groups') diff --git a/gnuviechadmin/osusers/models.py b/gnuviechadmin/osusers/models.py index 1b0d567..d5a65ef 100644 --- a/gnuviechadmin/osusers/models.py +++ b/gnuviechadmin/osusers/models.py @@ -1,3 +1,7 @@ +""" +This module defines the database models of operating system users. + +""" from __future__ import unicode_literals from datetime import date @@ -30,7 +34,7 @@ from .tasks import ( ) -logger = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__name__) CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _( @@ -38,8 +42,19 @@ CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _( class GroupManager(models.Manager): + """ + Manager class for :py:class:`osusers.models.Group`. + + """ def get_next_gid(self): + """ + Get the next available group id. + + :returns: group id + :rtype: int + + """ q = self.aggregate(models.Max('gid')) if q['gid__max'] is None: return settings.OSUSER_MINGID @@ -48,6 +63,10 @@ class GroupManager(models.Manager): @python_2_unicode_compatible class Group(TimeStampedModel, models.Model): + """ + This entity class corresponds to an operating system group. + + """ groupname = models.CharField( _('Group name'), max_length=16, unique=True) gid = models.PositiveSmallIntegerField( @@ -67,34 +86,73 @@ class Group(TimeStampedModel, models.Model): @transaction.atomic def save(self, *args, **kwargs): + """ + Save the group to the database and synchronizes group information to + LDAP. + + :param args: positional arguments to be passed on to + :py:meth:`django.db.Model.save` + :param kwargs: keyword arguments to be passed on to + :py:meth:`django.db.Model.save` + :return: self + :rtype: :py:class:`osusers.models.Group` + + """ super(Group, self).save(*args, **kwargs) dn = create_ldap_group.delay( self.groupname, self.gid, self.descr).get() - logger.info("created LDAP group with dn %s", dn) + _LOGGER.info("created LDAP group with dn %s", dn) return self @transaction.atomic def delete(self, *args, **kwargs): + """ + Delete the group from LDAP and the database. + + :param args: positional arguments to be passed on to + :py:meth:`django.db.Model.delete` + :param kwargs: keyword arguments to be passed on to + :py:meth:`django.db.Model.delete` + + """ delete_ldap_group_if_empty.delay(self.groupname).get() super(Group, self).delete(*args, **kwargs) class UserManager(models.Manager): + """ + Manager class for :py:class:`osusers.models.User`. + + """ def get_next_uid(self): + """ + Get the next available user id. + + :return: user id + :rtype: int + + """ q = self.aggregate(models.Max('uid')) if q['uid__max'] is None: return settings.OSUSER_MINUID return max(settings.OSUSER_MINUID, q['uid__max'] + 1) def get_next_username(self): + """ + Get the next available user name. + + :return: user name + :rtype: str + + """ count = 1 usernameformat = "{0}{1:02d}" nextuser = usernameformat.format(settings.OSUSER_USERNAME_PREFIX, count) for user in self.values('username').filter( - username__startswith=settings.OSUSER_USERNAME_PREFIX).order_by( - 'username'): + username__startswith=settings.OSUSER_USERNAME_PREFIX + ).order_by('username'): if user['username'] == nextuser: count += 1 nextuser = usernameformat.format( @@ -107,6 +165,23 @@ class UserManager(models.Manager): def create_user( self, customer, username=None, password=None, commit=False ): + """ + Create a new user with a primary group named the same as the user and + an initial password. + + If username is None the result of :py:meth:`get_next_username` is used. + If password is None a new password will be generated using passlib's + :py:func:`generate_password`. + + :param customer: Django User instance this user is associated to + :param str username: the username or None + :param str password: the password or None + :param boolean commit: whether to commit the user data to the database + or not + :return: new user + :rtype: :py:class:`osusers.models.User` + + """ uid = self.get_next_uid() gid = Group.objects.get_next_gid() if username is None: @@ -126,6 +201,10 @@ class UserManager(models.Manager): @python_2_unicode_compatible class User(TimeStampedModel, models.Model): + """ + This entity class corresponds to an operating system user. + + """ username = models.CharField( _('User name'), max_length=64, unique=True) uid = models.PositiveSmallIntegerField( @@ -147,6 +226,15 @@ class User(TimeStampedModel, models.Model): @transaction.atomic def set_password(self, password): + """ + Set the password of the user. + + The password is set to the user's + :py:class:`Shadow ` instance and to LDAP. + + :param str password: the new password + + """ if hasattr(self, 'shadow'): self.shadow.set_password(password) else: @@ -161,12 +249,24 @@ class User(TimeStampedModel, models.Model): @transaction.atomic def save(self, *args, **kwargs): + """ + Save the user to the database, create user directories and synchronize + user information to LDAP. + + :param args: positional arguments to be passed on to + :py:meth:`django.db.Model.save` + :param kwargs: keyword arguments to be passed on to + :py:meth:`django.db.Model.save` + :return: self + :rtype: :py:class:`osusers.models.User` + + """ dn = create_ldap_user.delay( self.username, self.uid, self.group.gid, self.gecos, self.homedir, self.shell, password=None).get() sftp_dir = setup_file_sftp_userdir.delay(self.username).get() mail_dir = setup_file_mail_userdir.delay(self.username).get() - logger.info( + _LOGGER.info( "created user %(user)s with LDAP dn %(dn)s, home directory " "%(homedir)s and mail base directory %(maildir)s.", { 'user': self, 'dn': dn, @@ -176,6 +276,16 @@ class User(TimeStampedModel, models.Model): @transaction.atomic def delete(self, *args, **kwargs): + """ + Delete the user and its groups from LDAP and the database and remove + the user's directories. + + :param args: positional arguments to be passed on to + :py:meth:`django.db.Model.delete` + :param kwargs: keyword arguments to be passed on to + :py:meth:`django.db.Model.delete` + + """ delete_file_mail_userdir.delay(self.username).get() delete_file_sftp_userdir.delay(self.username).get() for group in [ag.group for ag in self.additionalgroup_set.all()]: @@ -187,9 +297,23 @@ class User(TimeStampedModel, models.Model): class ShadowManager(models.Manager): + """ + Manager class for :py:class:`osusers.models.Shadow`. + + """ @transaction.atomic def create_shadow(self, user, password): + """ + Create a new shadow instance with typical Linux settings for the given + user with the given password. + + :param user: :py:class:`User ` instance + :param str password: the password + :return: new Shadow instance + :rtype: :py:class:`osusers.models.Shadow` instance + + """ changedays = (timezone.now().date() - date(1970, 1, 1)).days shadow = self.create( user=user, changedays=changedays, @@ -203,6 +327,11 @@ class ShadowManager(models.Manager): @python_2_unicode_compatible class Shadow(TimeStampedModel, models.Model): + """ + This entity class corresponds to an operating system user's shadow file + entry. + + """ user = models.OneToOneField(User, primary_key=True, verbose_name=_('User')) passwd = models.CharField(_('Encrypted password'), max_length=128) changedays = models.PositiveSmallIntegerField( @@ -245,11 +374,21 @@ class Shadow(TimeStampedModel, models.Model): return 'for user {0}'.format(self.user) def set_password(self, password): + """ + Set and encrypt the password. + + :param str password: the password + """ self.passwd = sha512_crypt.encrypt(password) @python_2_unicode_compatible class AdditionalGroup(TimeStampedModel, models.Model): + """ + This entity class corresponds to additional group assignments for an + :py:class:`operating system user `. + + """ user = models.ForeignKey(User) group = models.ForeignKey(Group) @@ -258,21 +397,45 @@ class AdditionalGroup(TimeStampedModel, models.Model): verbose_name = _('Additional group') verbose_name_plural = _('Additional groups') + def __str__(self): + return '{0} in {1}'.format(self.user, self.group) + def clean(self): + """ + Ensure that the assigned group is different from the user's primary + group. + + """ if self.user.group == self.group: raise ValidationError(CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL) @transaction.atomic def save(self, *args, **kwargs): + """ + Persists the group assignment to LDAP and the database. + + :param args: positional arguments to be passed on to + :py:meth:`django.db.Model.save` + :param kwargs: keyword arguments to be passed on to + :py:meth:`django.db.Model.save` + :return: this instance + :rtype: :py:class:`AdditionalGroup ` + + """ add_ldap_user_to_group.delay( self.user.username, self.group.groupname).get() - super(AdditionalGroup, self).save(*args, **kwargs) + return super(AdditionalGroup, self).save(*args, **kwargs) @transaction.atomic def delete(self, *args, **kwargs): + """ + Delete the group assignment from LDAP and the database. + + :param args: positional arguments to be passed on to + :py:meth:`django.db.Model.delete` + :param kwargs: keyword arguments to be passed on to + :py:meth:`django.db.Model.delete` + """ remove_ldap_user_from_group.delay( self.user.username, self.group.groupname).get() super(AdditionalGroup, self).delete(*args, **kwargs) - - def __str__(self): - return '{0} in {1}'.format(self.user, self.group) diff --git a/gnuviechadmin/osusers/tasks.py b/gnuviechadmin/osusers/tasks.py index 99c7b1c..ce250ab 100644 --- a/gnuviechadmin/osusers/tasks.py +++ b/gnuviechadmin/osusers/tasks.py @@ -1,3 +1,10 @@ +""" +This module defines task stubs for the tasks implemented on the `Celery`_ +workers. + +.. _Celery: http://www.celeryproject.org/ + +""" from __future__ import absolute_import from celery import shared_task @@ -5,59 +12,182 @@ from celery import shared_task @shared_task def create_ldap_group(groupname, gid, descr): - pass + """ + This task creates an :py:class:`LDAP group ` + if it does not exist yet. + + If a group with the given name exists its group id and description + attributes are updated. + + :param str groupname: the group name + :param int gid: the group id + :param str descr: description text for the group + :return: the distinguished name of the group + :rtype: str + """ @shared_task def create_ldap_user(username, uid, gid, gecos, homedir, shell, password): - pass + """ + This task creates an :py:class:`LDAP user ` + if it does not exist yet. + + The task is rejected if the primary group of the user is not defined. + + The user's fields are updated if the user already exists. + + :param str username: the user name + :param int uid: the user id + :param int gid: the user's primary group's id + :param str gecos: the text for the GECOS field + :param str homedir: the user's home directory + :param str shell: the user's login shell + :param str or None password: the clear text password, if :py:const:`None` + is passed the password is not touched + :raises celery.exceptions.Reject: if the specified primary group does not + exist + :return: the distinguished name of the user + :rtype: str + + """ @shared_task def add_ldap_user_to_group(username, groupname): - pass + """ + This task adds the specified user to the given group. + + This task does nothing if the user is already member of the group. + + :param str username: the user name + :param str groupname: the group name + :raises celery.exceptions.Retry: if the user does not exist yet, + :py:func:`create_ldap_user` should be called before + :return: True if the user has been added to the group otherwise False + :rtype: boolean + + """ @shared_task def remove_ldap_user_from_group(username, groupname): - pass + """ + This task removes the given user from the given group. + + :param str username: the user name + :param str groupname: the group name + :return: True if the user has been removed, False otherwise + :rtype: boolean + + """ @shared_task def delete_ldap_user(username): - pass + """ + This task deletes the given user. + + :param str username: the user name + :return: True if the user has been deleted, False otherwise + :rtype: boolean + + """ @shared_task def delete_ldap_group_if_empty(groupname): - pass + """ + This task deletes the given group. + + :param str groupname: the group name + :return: True if the user has been deleted, False otherwise + :rtype: boolean + + """ @shared_task def setup_file_sftp_userdir(username): - pass + """ + This task creates the home directory for an SFTP user if it does not exist + yet. + + :param str username: the user name + :raises Exception: if the SFTP directory of the user cannot be created + :return: the created directory name + :rtype: str + + """ @shared_task def delete_file_sftp_userdir(username): - pass + """ + This task recursively deletes the home directory of an SFTP user if it + does not exist yet. + + :param str username: the user name + :raises Exception: if the SFTP directory of the user cannot be removed + :return: the removed directory name + :rtype: str + + """ @shared_task def setup_file_mail_userdir(username): - pass + """ + This task creates the mail base directory for a user if it does not exist + yet. + + :param str username: the user name + :raises Exception: if the mail base directory for the user cannot be + created + :return: the created directory name + :rtype: str + + """ @shared_task def delete_file_mail_userdir(username): - pass + """ + This task recursively deletes the mail base directory for a user if it + does not exist yet. + + :param str username: the user name + :raises Exception: if the mail base directory of the user cannot be removed + :return: the removed directory name + :rtype: str + + """ @shared_task def create_file_mailbox(username, mailboxname): - pass + """ + This task creates a new mailbox directory for the given user and mailbox + name. + + :param str username: the user name + :param str mailboxname: the mailbox name + :raises GVAFileException: if the mailbox directory cannot be created + :return: the created mailbox directory name + :rtype: str + + """ @shared_task def delete_file_mailbox(username, mailboxname): - pass + """ + This task deletes the given mailbox of the given user. + + :param str username: the user name + :param str mailboxname: the mailbox name + :raises GVAFileException: if the mailbox directory cannot be deleted + :return: the deleted mailbox directory name + :rtype: str + + """ From 48d509c5db42477a20eefdf3327eaf783a21fc7d Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 27 Dec 2014 22:44:45 +0100 Subject: [PATCH 4/5] add code documentation to docs --- docs/code.rst | 150 +++++++++++++++++++++++++++++++++++++++++++++++++ docs/conf.py | 11 +++- docs/index.rst | 1 + 3 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 docs/code.rst diff --git a/docs/code.rst b/docs/code.rst new file mode 100644 index 0000000..14c6e08 --- /dev/null +++ b/docs/code.rst @@ -0,0 +1,150 @@ +****************** +Code documentation +****************** + +.. index:: Django + +gva is implemented as `Django`_ project and provides a frontend for +administrators and customers. + +.. _Django: https://www.djangoproject.com/ + + +The project module :py:mod:`gnuviechadmin` +========================================== + +.. automodule:: gnuviechadmin + + +:py:mod:`celery ` +--------------------------------------- + +.. automodule:: gnuviechadmin.celery + :members: + + +:py:mod:`urls ` +----------------------------------- + +.. automodule:: gnuviechadmin.urls + + +:py:mod:`wsgi ` +----------------------------------- + +.. automodule:: gnuviechadmin.wsgi + :members: + + +:py:mod:`settings ` +------------------------------------------- + +.. automodule:: gnuviechadmin.settings + + +:py:mod:`base ` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: gnuviechadmin.settings.base + :members: + + +:py:mod:`local ` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: gnuviechadmin.settings.local + + +:py:mod:`production ` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: gnuviechadmin.settings.production + + +:py:mod:`test ` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: gnuviechadmin.settings.test + + +:py:mod:`gvacommon` +=================== + +This module is imported from a separate git project via git subtree and +provides some functionality that is common to all gnuviechadmin subprojects. + +.. automodule:: gvacommon + + +:py:mod:`celeryrouters ` +------------------------------------------------- + +.. automodule:: gvacommon.celeryrouters + :members: + :undoc-members: + + +:py:mod:`managemails` app +========================= + +.. automodule:: managemails + + +:py:mod:`admin ` +----------------------------------- + +.. automodule:: managemails.admin + :members: + + +:py:mod:`models ` +------------------------------------- + +.. automodule:: managemails.models + :members: + + +:py:mod:`osusers` app +===================== + +.. automodule:: osusers + + +:py:mod:`admin ` +------------------------------- + +.. automodule:: osusers.admin + :members: + + +:py:mod:`apps ` +----------------------------- + +.. automodule:: osusers.apps + :members: + + +:py:mod:`models ` +--------------------------------- + +.. automodule:: osusers.models + :members: + + +:py:mod:`tasks ` +------------------------------- + +.. automodule:: osusers.tasks + +.. autotask:: osusers.tasks.add_ldap_user_to_group +.. autotask:: osusers.tasks.create_file_mailbox +.. autotask:: osusers.tasks.create_ldap_group +.. autotask:: osusers.tasks.create_ldap_user +.. autotask:: osusers.tasks.delete_file_mail_userdir +.. autotask:: osusers.tasks.delete_file_mailbox +.. autotask:: osusers.tasks.delete_file_sftp_userdir +.. autotask:: osusers.tasks.delete_ldap_group_if_empty +.. autotask:: osusers.tasks.delete_ldap_user +.. autotask:: osusers.tasks.remove_ldap_user_from_group +.. autotask:: osusers.tasks.setup_file_mail_userdir +.. autotask:: osusers.tasks.setup_file_sftp_userdir diff --git a/docs/conf.py b/docs/conf.py index ffdf565..748e1c4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,13 +13,18 @@ # All configuration values have a default; values that are commented out # serve to show the default. -#import sys -#import os +import sys +import os +import django # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath(os.path.join('..', 'gnuviechadmin'))) + +os.environ['GVA_SITE_ADMINMAIL'] = 'admin@gva.example.org' + +django.setup() # -- General configuration ----------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 0d9e0c2..3f1d47e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,6 +14,7 @@ Contents: install deploy tests + code changelog From d4e62bf6f31f8628d2395d14ed27694675121445 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 27 Dec 2014 22:58:57 +0100 Subject: [PATCH 5/5] add german translation --- .gitignore | 1 + .../locale/de/LC_MESSAGES/django.po | 60 ++++++ .../osusers/locale/de/LC_MESSAGES/django.po | 175 ++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po create mode 100644 gnuviechadmin/osusers/locale/de/LC_MESSAGES/django.po diff --git a/.gitignore b/.gitignore index f25647b..ca72d2e 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ Desktop.ini htmlcov/ tags _build/ +*.mo diff --git a/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..442c816 --- /dev/null +++ b/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,60 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: managemails\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-12-27 22:45+0100\n" +"PO-Revision-Date: 2014-12-27 22:57+0100\n" +"Last-Translator: Jan Dittberner \n" +"Language-Team: de \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.6.10\n" +"X-Poedit-SourceCharset: UTF-8\n" + +#: admin.py:14 +msgid "Passwords don't match" +msgstr "Passwörter stimmen nicht überein" + +#: admin.py:21 tests/test_admin.py:37 +msgid "Hash" +msgstr "Hash-Code" + +#: admin.py:44 +msgid "Password" +msgstr "Passwort" + +#: admin.py:46 +msgid "Password (again)" +msgstr "Passwortwiederholung" + +#: admin.py:100 +msgid "Activate" +msgstr "Aktivieren" + +#: admin.py:101 +msgid "Deactivate" +msgstr "Deaktivieren" + +#: models.py:51 +msgid "Mailbox" +msgstr "Postfach" + +#: models.py:52 +msgid "Mailboxes" +msgstr "Postfächer" + +#: models.py:76 +msgid "Mail address" +msgstr "E-Mailadresse" + +#: models.py:77 +msgid "Mail addresses" +msgstr "E-Mailadressen" diff --git a/gnuviechadmin/osusers/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/osusers/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..b6919a5 --- /dev/null +++ b/gnuviechadmin/osusers/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,175 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: osusers\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-12-27 22:46+0100\n" +"PO-Revision-Date: 2014-12-27 22:54+0100\n" +"Last-Translator: Jan Dittberner \n" +"Language-Team: de \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.6.10\n" +"X-Poedit-SourceCharset: UTF-8\n" + +#: admin.py:16 +msgid "Passwords don't match" +msgstr "Passwörter stimmen nicht überein" + +#: admin.py:47 +msgid "Password" +msgstr "Passwort" + +#: admin.py:51 +msgid "Password (again)" +msgstr "Passwortwiederholung" + +#: admin.py:160 +msgid "Delete selected users" +msgstr "Ausgewählte Nutzer löschen" + +#: admin.py:201 +msgid "Delete selected groups" +msgstr "Ausgewählte Gruppen löschen" + +#: apps.py:17 +msgid "Operating System Users and Groups" +msgstr "Betriebssystemnutzer- und Gruppen" + +#: models.py:41 +msgid "You can not use a user's primary group." +msgstr "Sie können nicht die primäre Gruppe des Nutzers verwenden." + +#: models.py:71 +msgid "Group name" +msgstr "Gruppenname" + +#: models.py:73 +msgid "Group ID" +msgstr "Gruppen-ID" + +#: models.py:74 +msgid "Description" +msgstr "Beschreibung" + +#: models.py:76 +msgid "Group password" +msgstr "Gruppenpasswort" + +#: models.py:81 models.py:212 +msgid "Group" +msgstr "Gruppe" + +#: models.py:82 +msgid "Groups" +msgstr "Gruppen" + +#: models.py:209 +msgid "User name" +msgstr "Nutzername" + +#: models.py:211 +msgid "User ID" +msgstr "Nutzer-ID" + +#: models.py:213 +msgid "Gecos field" +msgstr "GECOS-Feld" + +#: models.py:214 +msgid "Home directory" +msgstr "Home-Verzeichnis" + +#: models.py:215 +msgid "Login shell" +msgstr "Loginshell" + +#: models.py:221 models.py:335 +msgid "User" +msgstr "Nutzer" + +#: models.py:222 +msgid "Users" +msgstr "Nutzer" + +#: models.py:336 +msgid "Encrypted password" +msgstr "Verschlüsseltes Passwort" + +#: models.py:338 +msgid "Date of last change" +msgstr "Datum der letzten Änderung" + +#: models.py:339 +msgid "This is expressed in days since Jan 1, 1970" +msgstr "Ausgedrückt als Tage seit dem 1. Januar 1970" + +#: models.py:342 +msgid "Minimum age" +msgstr "Minimales Alter" + +#: models.py:343 +msgid "Minimum number of days before the password can be changed" +msgstr "Minmale Anzahl von Tagen bevor das Passwort geändert werden kann" + +#: models.py:347 +msgid "Maximum age" +msgstr "Maximales Alter" + +#: models.py:348 +msgid "Maximum number of days after which the password has to be changed" +msgstr "" +"Maximale Anzahl von Tagen, nach denen das Passwort geändert werden muss" + +#: models.py:352 +msgid "Grace period" +msgstr "Duldungsperiode" + +#: models.py:353 +msgid "The number of days before the password is going to expire" +msgstr "Anzahl von Tagen nach denen das Passwort verfällt" + +#: models.py:357 +msgid "Inactivity period" +msgstr "Inaktivitätsperiode" + +#: models.py:358 +msgid "" +"The number of days after the password has expired during which the password " +"should still be accepted" +msgstr "" +"Die Anzahl von Tagen für die ein verfallenes Passwort noch akzeptiert werden " +"soll" + +#: models.py:362 +msgid "Account expiration date" +msgstr "Kontoverfallsdatum" + +#: models.py:363 +msgid "" +"The date of expiration of the account, expressed as number of days since Jan " +"1, 1970" +msgstr "Kontoverfallsdatum in Tagen seit dem 1. Januar 1970" + +#: models.py:370 +msgid "Shadow password" +msgstr "Shadow-Passwort" + +#: models.py:371 +msgid "Shadow passwords" +msgstr "Shadow-Passwörter" + +#: models.py:397 +msgid "Additional group" +msgstr "Weitere Gruppe" + +#: models.py:398 +msgid "Additional groups" +msgstr "Weitere Gruppen"