From e87708712736a49eb81b770a449ebac0667a9300 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Mon, 22 Dec 2014 20:07:11 +0100 Subject: [PATCH 1/5] make user and group management more robust - remove TaskResultInline and subclasses - add custom perform_delete_selected action to UserAdmin and GroupAdmin - properly clean asynchronous tasks in rabbitmq - wrap user operations in transactions --- gnuviechadmin/osusers/admin.py | 94 ++++++++++++++++++++------------- gnuviechadmin/osusers/models.py | 40 +++++++++++--- 2 files changed, 91 insertions(+), 43 deletions(-) diff --git a/gnuviechadmin/osusers/admin.py b/gnuviechadmin/osusers/admin.py index 4e74a04..206b189 100644 --- a/gnuviechadmin/osusers/admin.py +++ b/gnuviechadmin/osusers/admin.py @@ -25,30 +25,6 @@ class ShadowInline(admin.TabularInline): can_delete = False -class TaskResultInline(admin.TabularInline): - can_delete = False - extra = 0 - readonly_fields = ['task_uuid', 'task_name', 'is_finished', 'is_success', - 'state', 'result_body'] - - def get_queryset(self, request): - qs = super(TaskResultInline, self).get_queryset(request) - for entry in qs: - entry.update_taskstatus() - return qs - - def has_add_permission(self, request, obj=None): - return False - - -class UserTaskResultInline(TaskResultInline): - model = UserTaskResult - - -class GroupTaskResultInline(TaskResultInline): - model = GroupTaskResult - - class UserCreationForm(forms.ModelForm): """ A form for creating system users. @@ -91,9 +67,10 @@ class UserCreationForm(forms.ModelForm): class UserAdmin(admin.ModelAdmin): - inlines = [AdditionalGroupInline, ShadowInline, UserTaskResultInline] + actions = ['perform_delete_selected'] readonly_fields = ['uid'] add_form = UserCreationForm + inlines = [AdditionalGroupInline, ShadowInline] add_fieldsets = ( (None, { @@ -115,37 +92,82 @@ class UserAdmin(admin.ModelAdmin): defaults.update(kwargs) return super(UserAdmin, self).get_form(request, obj, **defaults) - def get_inline_instances(self, request, obj=None): - if obj is None: - return [] - return super(UserAdmin, self).get_inline_instances(request, obj) + def get_readonly_fields(self, request, obj=None): + if obj: + return ['uid'] + return [] + + def perform_delete_selected(self, request, queryset): + for user in queryset.all(): + user.delete() + perform_delete_selected.short_description = _('Delete selected users') + + def get_actions(self, request): + actions = super(UserAdmin, self).get_actions(request) + if 'delete_selected' in actions: + del actions['delete_selected'] + return actions class GroupAdmin(admin.ModelAdmin): - inlines = [GroupTaskResultInline] + 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): + for group in queryset.all(): + group.delete() + perform_delete_selected.short_description = _('Delete selected groups') -class DeleteTaskResultAdmin(admin.ModelAdmin): - readonly_fields = ['task_uuid', 'task_name', 'modeltype', 'modelname', - 'is_finished', 'is_success', 'state', 'result_body'] - list_display = ('task_uuid', 'task_name', 'modeltype', 'modelname', - 'is_finished', 'state') + def get_actions(self, request): + actions = super(GroupAdmin, self).get_actions(request) + if 'delete_selected' in actions: + del actions['delete_selected'] + return actions + +class TaskResultAdmin(admin.ModelAdmin): def has_add_permission(self, request, obj=None): return False + def has_delete_permission(self, request, obj=None): + return obj is None or obj.is_finished + def get_queryset(self, request): - qs = super(DeleteTaskResultAdmin, self).get_queryset(request) + qs = super(TaskResultAdmin, self).get_queryset(request) for entry in qs: entry.update_taskstatus() return qs +class DeleteTaskResultAdmin(TaskResultAdmin): + readonly_fields = ['task_uuid', 'task_name', 'modeltype', 'modelname', + 'is_finished', 'is_success', 'state', 'result_body'] + list_display = ('task_uuid', 'task_name', 'modeltype', 'modelname', + 'is_finished', 'state') + + +class GroupTaskResultAdmin(TaskResultAdmin): + readonly_fields = [ + 'task_uuid', 'task_name', 'group', 'is_finished', 'is_success', + 'state', 'result_body' + ] + list_display = ('task_uuid', 'task_name', 'group', 'is_finished', 'state') + + +class UserTaskResultAdmin(TaskResultAdmin): + readonly_fields = [ + 'task_uuid', 'task_name', 'user', 'is_finished', 'is_success', 'state', + 'result_body' + ] + list_display = ('task_uuid', 'task_name', 'user', 'is_finished', 'state') + + admin.site.register(Group, GroupAdmin) admin.site.register(User, UserAdmin) admin.site.register(DeleteTaskResult, DeleteTaskResultAdmin) +admin.site.register(GroupTaskResult, GroupTaskResultAdmin) +admin.site.register(UserTaskResult, UserTaskResultAdmin) diff --git a/gnuviechadmin/osusers/models.py b/gnuviechadmin/osusers/models.py index 801f6ba..325d056 100644 --- a/gnuviechadmin/osusers/models.py +++ b/gnuviechadmin/osusers/models.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from datetime import date import os @@ -42,18 +44,17 @@ class TaskResult(TimeStampedModel, models.Model): abstract = True def _set_result_fields(self, asyncresult): - if asyncresult.ready(): + if not self.is_finished: + result = asyncresult.get(no_ack=False) self.is_finished = True self.is_success = asyncresult.state == 'SUCCESS' - self.result_body = str(asyncresult.result) self.state = asyncresult.state - asyncresult.get(no_ack=False) + self.result_body = str(result) def update_taskstatus(self): - if not self.is_finished: - asyncresult = AsyncResult(self.task_uuid) - self._set_result_fields(asyncresult) - self.save() + asyncresult = AsyncResult(self.task_uuid) + self._set_result_fields(asyncresult) + self.save() class GroupManager(models.Manager): @@ -84,6 +85,7 @@ class Group(TimeStampedModel, models.Model): def __str__(self): return '{0} ({1})'.format(self.groupname, self.gid) + @transaction.atomic def save(self, *args, **kwargs): super(Group, self).save(*args, **kwargs) GroupTaskResult.objects.create_grouptaskresult( @@ -93,6 +95,7 @@ class Group(TimeStampedModel, models.Model): ) return self + @transaction.atomic def delete(self, *args, **kwargs): DeleteTaskResult.objects.create_deletetaskresult( 'group', self.groupname, @@ -125,6 +128,7 @@ class DeleteTaskResultManager(TaskResultManager): return taskresult +@python_2_unicode_compatible class DeleteTaskResult(TaskResult): modeltype = models.CharField(max_length=20, db_index=True) @@ -132,6 +136,11 @@ class DeleteTaskResult(TaskResult): objects = DeleteTaskResultManager() + def __str__(self): + return "{task_uuid} {task_name} {modeltype} {modelname}".format( + task_uuid=self.task_uuid, task_name=self.task_name, + modeltype=self.modeltype, modelname=self.modelname) + class GroupTaskResultManager(TaskResultManager): @@ -145,12 +154,18 @@ class GroupTaskResultManager(TaskResultManager): return taskresult +@python_2_unicode_compatible class GroupTaskResult(TaskResult): group = models.ForeignKey(Group) objects = GroupTaskResultManager() + def __str__(self): + return "{task_uuid} {task_name} {group}".format( + task_uuid=self.task_uuid, task_name=self.task_name, + group=self.group) + class UserManager(models.Manager): @@ -215,6 +230,7 @@ class User(TimeStampedModel, models.Model): def __str__(self): return '{0} ({1})'.format(self.username, self.uid) + @transaction.atomic def set_password(self, password): if hasattr(self, 'shadow'): self.shadow.set_password(password) @@ -232,6 +248,7 @@ class User(TimeStampedModel, models.Model): commit=True ) + @transaction.atomic def save(self, *args, **kwargs): UserTaskResult.objects.create_usertaskresult( self, @@ -243,6 +260,7 @@ class User(TimeStampedModel, models.Model): ) return super(User, self).save(*args, **kwargs) + @transaction.atomic def delete(self, *args, **kwargs): for group in [ ag.group for ag in AdditionalGroup.objects.filter(user=self) @@ -274,15 +292,21 @@ class UserTaskResultManager(TaskResultManager): return taskresult +@python_2_unicode_compatible class UserTaskResult(TaskResult): user = models.ForeignKey(User) objects = UserTaskResultManager() + def __str__(self): + return "{task_uuid} {task_name} {user}".format( + task_uuid=self.task_uuid, task_name=self.task_name, user=self.user) + class ShadowManager(models.Manager): + @transaction.atomic def create_shadow(self, user, password): changedays = (timezone.now().date() - date(1970, 1, 1)).days shadow = self.create( @@ -356,6 +380,7 @@ class AdditionalGroup(TimeStampedModel, models.Model): if self.user.group == self.group: raise ValidationError(CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL) + @transaction.atomic def save(self, *args, **kwargs): GroupTaskResult.objects.create_grouptaskresult( self.group, @@ -365,6 +390,7 @@ class AdditionalGroup(TimeStampedModel, models.Model): ) super(AdditionalGroup, self).save(*args, **kwargs) + @transaction.atomic def delete(self, *args, **kwargs): DeleteTaskResult.objects.create_deletetaskresult( 'usergroup', From 2428a39f192564e3dd5397510848b3d1b6c1b93d Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Thu, 25 Dec 2014 18:05:42 +0100 Subject: [PATCH 2/5] add stub tasks for file system operations --- gnuviechadmin/gnuviechadmin/settings/base.py | 1 + gnuviechadmin/osusers/tasks.py | 30 ++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/gnuviechadmin/gnuviechadmin/settings/base.py b/gnuviechadmin/gnuviechadmin/settings/base.py index c871d84..e4dc147 100644 --- a/gnuviechadmin/gnuviechadmin/settings/base.py +++ b/gnuviechadmin/gnuviechadmin/settings/base.py @@ -280,6 +280,7 @@ CELERY_RESULT_PERSISTENT = True CELERY_TASK_RESULT_EXPIRES = None CELERY_ROUTES = ( 'osusers.tasks.LdapRouter', + 'osusers.tasks.FileRouter', ) CELERY_ACCEPT_CONTENT = ['yaml'] CELERY_TASK_SERIALIZER = 'yaml' diff --git a/gnuviechadmin/osusers/tasks.py b/gnuviechadmin/osusers/tasks.py index 049eb65..7edd7c7 100644 --- a/gnuviechadmin/osusers/tasks.py +++ b/gnuviechadmin/osusers/tasks.py @@ -13,6 +13,16 @@ class LdapRouter(object): return None +class FileRouter(object): + + def route_for_task(self, task, args=None, kwargs=None): + if 'file' in task: + return {'exchange': 'file', + 'exchange_type': 'direct', + 'queue': 'file'} + return None + + @shared_task def create_ldap_group(groupname, gid, descr): pass @@ -41,3 +51,23 @@ def delete_ldap_user(username): @shared_task def delete_ldap_group_if_empty(groupname): pass + + +@shared_task +def setup_file_sftp_userdir(username): + pass + + +@shared_task +def delete_file_sftp_userdir(username): + pass + + +@shared_task +def setup_file_mail_userdir(username): + pass + + +@shared_task +def delete_file_mail_userdir(username): + pass From 0b6ac2a478cac1ef656c8fd4231063e527856885 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Fri, 26 Dec 2014 15:10:36 +0100 Subject: [PATCH 3/5] create directories for new users - use new file tasks to create SFTP and mail base directories for users - use json serializer as default - remove TaskResult classes that don't provide any significant benefit --- gnuviechadmin/gnuviechadmin/settings/base.py | 6 +- gnuviechadmin/osusers/admin.py | 43 ---- .../migrations/0002_auto_20141226_1456.py | 31 +++ gnuviechadmin/osusers/models.py | 204 ++++-------------- 4 files changed, 72 insertions(+), 212 deletions(-) create mode 100644 gnuviechadmin/osusers/migrations/0002_auto_20141226_1456.py diff --git a/gnuviechadmin/gnuviechadmin/settings/base.py b/gnuviechadmin/gnuviechadmin/settings/base.py index e4dc147..5442654 100644 --- a/gnuviechadmin/gnuviechadmin/settings/base.py +++ b/gnuviechadmin/gnuviechadmin/settings/base.py @@ -282,9 +282,9 @@ CELERY_ROUTES = ( 'osusers.tasks.LdapRouter', 'osusers.tasks.FileRouter', ) -CELERY_ACCEPT_CONTENT = ['yaml'] -CELERY_TASK_SERIALIZER = 'yaml' -CELERY_RESULT_SERIALIZER = 'yaml' +CELERY_ACCEPT_CONTENT = ['pickle', 'yaml', 'json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_SERIALIZER = 'json' ########## END CELERY CONFIGURATION diff --git a/gnuviechadmin/osusers/admin.py b/gnuviechadmin/osusers/admin.py index 206b189..aa85d96 100644 --- a/gnuviechadmin/osusers/admin.py +++ b/gnuviechadmin/osusers/admin.py @@ -4,12 +4,9 @@ from django.contrib import admin from .models import ( AdditionalGroup, - DeleteTaskResult, Group, - GroupTaskResult, Shadow, User, - UserTaskResult, ) PASSWORD_MISMATCH_ERROR = _("Passwords don't match") @@ -129,45 +126,5 @@ class GroupAdmin(admin.ModelAdmin): return actions -class TaskResultAdmin(admin.ModelAdmin): - def has_add_permission(self, request, obj=None): - return False - - def has_delete_permission(self, request, obj=None): - return obj is None or obj.is_finished - - def get_queryset(self, request): - qs = super(TaskResultAdmin, self).get_queryset(request) - for entry in qs: - entry.update_taskstatus() - return qs - - -class DeleteTaskResultAdmin(TaskResultAdmin): - readonly_fields = ['task_uuid', 'task_name', 'modeltype', 'modelname', - 'is_finished', 'is_success', 'state', 'result_body'] - list_display = ('task_uuid', 'task_name', 'modeltype', 'modelname', - 'is_finished', 'state') - - -class GroupTaskResultAdmin(TaskResultAdmin): - readonly_fields = [ - 'task_uuid', 'task_name', 'group', 'is_finished', 'is_success', - 'state', 'result_body' - ] - list_display = ('task_uuid', 'task_name', 'group', 'is_finished', 'state') - - -class UserTaskResultAdmin(TaskResultAdmin): - readonly_fields = [ - 'task_uuid', 'task_name', 'user', 'is_finished', 'is_success', 'state', - 'result_body' - ] - list_display = ('task_uuid', 'task_name', 'user', 'is_finished', 'state') - - admin.site.register(Group, GroupAdmin) admin.site.register(User, UserAdmin) -admin.site.register(DeleteTaskResult, DeleteTaskResultAdmin) -admin.site.register(GroupTaskResult, GroupTaskResultAdmin) -admin.site.register(UserTaskResult, UserTaskResultAdmin) diff --git a/gnuviechadmin/osusers/migrations/0002_auto_20141226_1456.py b/gnuviechadmin/osusers/migrations/0002_auto_20141226_1456.py new file mode 100644 index 0000000..0c90043 --- /dev/null +++ b/gnuviechadmin/osusers/migrations/0002_auto_20141226_1456.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('osusers', '0001_initial'), + ] + + operations = [ + migrations.DeleteModel( + name='DeleteTaskResult', + ), + migrations.RemoveField( + model_name='grouptaskresult', + name='group', + ), + migrations.DeleteModel( + name='GroupTaskResult', + ), + migrations.RemoveField( + model_name='usertaskresult', + name='user', + ), + migrations.DeleteModel( + name='UserTaskResult', + ), + ] diff --git a/gnuviechadmin/osusers/models.py b/gnuviechadmin/osusers/models.py index 325d056..f3a524b 100644 --- a/gnuviechadmin/osusers/models.py +++ b/gnuviechadmin/osusers/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from datetime import date +import logging import os from django.db import models, transaction @@ -21,42 +22,23 @@ from .tasks import ( add_ldap_user_to_group, create_ldap_group, create_ldap_user, + delete_file_mail_userdir, + delete_file_sftp_userdir, delete_ldap_group_if_empty, delete_ldap_user, remove_ldap_user_from_group, + setup_file_mail_userdir, + setup_file_sftp_userdir, ) +logger = logging.getLogger(__name__) + + CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _( "You can not use a user's primary group.") -class TaskResult(TimeStampedModel, models.Model): - - task_uuid = models.CharField(primary_key=True, max_length=64, blank=False) - task_name = models.CharField(max_length=255, blank=False, db_index=True) - is_finished = models.BooleanField(default=False) - is_success = models.BooleanField(default=False) - state = models.CharField(max_length=10) - result_body = models.TextField(blank=True) - - class Meta: - abstract = True - - def _set_result_fields(self, asyncresult): - if not self.is_finished: - result = asyncresult.get(no_ack=False) - self.is_finished = True - self.is_success = asyncresult.state == 'SUCCESS' - self.state = asyncresult.state - self.result_body = str(result) - - def update_taskstatus(self): - asyncresult = AsyncResult(self.task_uuid) - self._set_result_fields(asyncresult) - self.save() - - class GroupManager(models.Manager): def get_next_gid(self): @@ -88,85 +70,17 @@ class Group(TimeStampedModel, models.Model): @transaction.atomic def save(self, *args, **kwargs): super(Group, self).save(*args, **kwargs) - GroupTaskResult.objects.create_grouptaskresult( - self, - create_ldap_group.delay(self.groupname, self.gid, self.descr), - 'create_ldap_group' - ) + dn = create_ldap_group.delay( + self.groupname, self.gid, self.descr).get() + logger.info("created LDAP group with dn %s", dn) return self @transaction.atomic def delete(self, *args, **kwargs): - DeleteTaskResult.objects.create_deletetaskresult( - 'group', self.groupname, - delete_ldap_group_if_empty.delay(self.groupname), - 'delete_ldap_group_if_empty' - ) + delete_ldap_group_if_empty.delay(self.groupname).get() super(Group, self).delete(*args, **kwargs) -class TaskResultManager(models.Manager): - - def create(self, asyncresult, task_name): - result = self.model( - task_uuid=asyncresult.task_id, task_name=task_name - ) - result._set_result_fields(asyncresult) - return result - - -class DeleteTaskResultManager(TaskResultManager): - - def create_deletetaskresult( - self, modeltype, modelname, asyncresult, task_name - ): - taskresult = super(DeleteTaskResultManager, self).create( - asyncresult, task_name) - taskresult.modeltype = modeltype - taskresult.modelname = modelname - taskresult.save() - return taskresult - - -@python_2_unicode_compatible -class DeleteTaskResult(TaskResult): - - modeltype = models.CharField(max_length=20, db_index=True) - modelname = models.CharField(max_length=255) - - objects = DeleteTaskResultManager() - - def __str__(self): - return "{task_uuid} {task_name} {modeltype} {modelname}".format( - task_uuid=self.task_uuid, task_name=self.task_name, - modeltype=self.modeltype, modelname=self.modelname) - - -class GroupTaskResultManager(TaskResultManager): - - def create_grouptaskresult( - self, group, asyncresult, task_name, commit=False - ): - taskresult = super(GroupTaskResultManager, self).create( - asyncresult, task_name) - taskresult.group = group - taskresult.save() - return taskresult - - -@python_2_unicode_compatible -class GroupTaskResult(TaskResult): - - group = models.ForeignKey(Group) - - objects = GroupTaskResultManager() - - def __str__(self): - return "{task_uuid} {task_name} {group}".format( - task_uuid=self.task_uuid, task_name=self.task_name, - group=self.group) - - class UserManager(models.Manager): def get_next_uid(self): @@ -238,72 +152,39 @@ class User(TimeStampedModel, models.Model): self.shadow = Shadow.objects.create_shadow( user=self, password=password ) - UserTaskResult.objects.create_usertaskresult( - self, - create_ldap_user.delay( - self.username, self.uid, self.group.gid, self.gecos, - self.homedir, self.shell, password - ), - 'create_ldap_user', - commit=True - ) + dn = create_ldap_user.delay( + self.username, self.uid, self.group.gid, self.gecos, + self.homedir, self.shell, password + ).get() + logging.info("set LDAP password for %s", dn) @transaction.atomic def save(self, *args, **kwargs): - UserTaskResult.objects.create_usertaskresult( - self, - create_ldap_user.delay( - self.username, self.uid, self.group.gid, self.gecos, - self.homedir, self.shell, password=None - ), - 'create_ldap_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( + "created user %(user)s with LDAP dn %(dn)s, home directory " + "%(homedir)s and mail base directory %(maildir)s.", { + 'user': self, 'dn': dn, + 'homedir': sftp_dir, 'maildir': mail_dir + }) return super(User, self).save(*args, **kwargs) @transaction.atomic def delete(self, *args, **kwargs): - for group in [ - ag.group for ag in AdditionalGroup.objects.filter(user=self) - ]: - DeleteTaskResult.objects.create_deletetaskresult( - 'usergroup', - '{0} in {1}'.format(self.username, group.groupname), - remove_ldap_user_from_group.delay( - self.username, group.groupname), - 'remove_ldap_user_from_group', - ) - DeleteTaskResult.objects.create_deletetaskresult( - 'user', self.username, - delete_ldap_user.delay(self.username), - 'delete_ldap_user' - ) + 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()]: + remove_ldap_user_from_group.delay( + self.username, group.groupname).get() + delete_ldap_user.delay(self.username).get() self.group.delete() super(User, self).delete(*args, **kwargs) -class UserTaskResultManager(TaskResultManager): - - def create_usertaskresult( - self, user, asyncresult, task_name, commit=False - ): - taskresult = self.create(asyncresult, task_name) - taskresult.user = user - taskresult.save() - return taskresult - - -@python_2_unicode_compatible -class UserTaskResult(TaskResult): - - user = models.ForeignKey(User) - - objects = UserTaskResultManager() - - def __str__(self): - return "{task_uuid} {task_name} {user}".format( - task_uuid=self.task_uuid, task_name=self.task_name, user=self.user) - - class ShadowManager(models.Manager): @transaction.atomic @@ -382,23 +263,14 @@ class AdditionalGroup(TimeStampedModel, models.Model): @transaction.atomic def save(self, *args, **kwargs): - GroupTaskResult.objects.create_grouptaskresult( - self.group, - add_ldap_user_to_group.delay( - self.user.username, self.group.groupname), - 'add_ldap_user_to_group' - ) + add_ldap_user_to_group.delay( + self.user.username, self.group.groupname).get() super(AdditionalGroup, self).save(*args, **kwargs) @transaction.atomic def delete(self, *args, **kwargs): - DeleteTaskResult.objects.create_deletetaskresult( - 'usergroup', - str(self), - remove_ldap_user_from_group.delay( - self.user.username, self.group.groupname), - 'remove_ldap_user_from_group' - ) + remove_ldap_user_from_group.delay( + self.user.username, self.group.groupname).get() super(AdditionalGroup, self).delete(*args, **kwargs) def __str__(self): From 08ab693b658962233b21e8f3b7645b5a944e45e1 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Fri, 26 Dec 2014 15:13:52 +0100 Subject: [PATCH 4/5] document homedir creation feature --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7f82ab1..51f484a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,8 @@ Changelog ========= +* :feature:`-` home and mail base directory creation + * :release:`0.2.1 <2014-12-17>` * :support:`-` update Django to 1.7.1, update other dependencies, drop South * :bug:`-` wrap :py:meth:`ousers.models.UserManager.create_user` in From 7c3efb2e504d16a6bcb1fa5f8ebd9e407ef2662b Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Fri, 26 Dec 2014 15:16:03 +0100 Subject: [PATCH 5/5] set version number --- docs/changelog.rst | 1 + docs/conf.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 51f484a..70036d8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* :release:`0.2.2 <2014-12-26>` * :feature:`-` home and mail base directory creation * :release:`0.2.1 <2014-12-17>` diff --git a/docs/conf.py b/docs/conf.py index 6b5720c..bb980fe 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,9 +55,9 @@ copyright = u'2014, Jan Dittberner' # built documents. # # The short X.Y version. -version = '0.2.1' +version = '0.2.2' # The full version, including alpha/beta/rc tags. -release = '0.2.1' +release = '0.2.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages.