Added support for raw hash password edditing
This commit is contained in:
parent
e804a1d102
commit
3e1d9f7d22
4
TODO.md
4
TODO.md
|
@ -460,3 +460,7 @@ with open(file) as handler:
|
||||||
|
|
||||||
|
|
||||||
# change filter By PHP version: by detail
|
# change filter By PHP version: by detail
|
||||||
|
|
||||||
|
# Mark transaction process as executed should not override higher transaction states
|
||||||
|
|
||||||
|
# Show password and set password management commands -sam -A|--all --systemuser --account --mailbox vs raw passwords on forms
|
||||||
|
|
|
@ -3,6 +3,7 @@ from functools import partial
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
|
from django.contrib.auth.hashers import identify_hasher
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.forms.models import modelformset_factory, BaseModelFormSet
|
from django.forms.models import modelformset_factory, BaseModelFormSet
|
||||||
from django.template import Template, Context
|
from django.template import Template, Context
|
||||||
|
@ -84,8 +85,11 @@ class AdminPasswordChangeForm(forms.Form):
|
||||||
error_messages = {
|
error_messages = {
|
||||||
'password_mismatch': _("The two password fields didn't match."),
|
'password_mismatch': _("The two password fields didn't match."),
|
||||||
'password_missing': _("No password has been provided."),
|
'password_missing': _("No password has been provided."),
|
||||||
|
'bad_hash': _("Invalid password format or unknown hashing algorithm."),
|
||||||
}
|
}
|
||||||
required_css_class = 'required'
|
required_css_class = 'required'
|
||||||
|
password = forms.CharField(label=_("Password"), required=False,
|
||||||
|
widget=forms.TextInput(attrs={'size':'120'}))
|
||||||
password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput,
|
password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput,
|
||||||
required=False, validators=[validate_password])
|
required=False, validators=[validate_password])
|
||||||
password2 = forms.CharField(label=_("Password (again)"), widget=forms.PasswordInput,
|
password2 = forms.CharField(label=_("Password (again)"), widget=forms.PasswordInput,
|
||||||
|
@ -93,9 +97,14 @@ class AdminPasswordChangeForm(forms.Form):
|
||||||
|
|
||||||
def __init__(self, user, *args, **kwargs):
|
def __init__(self, user, *args, **kwargs):
|
||||||
self.related = kwargs.pop('related', [])
|
self.related = kwargs.pop('related', [])
|
||||||
|
self.raw = kwargs.pop('raw', False)
|
||||||
self.user = user
|
self.user = user
|
||||||
super(AdminPasswordChangeForm, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.password_provided = False
|
||||||
for ix, rel in enumerate(self.related):
|
for ix, rel in enumerate(self.related):
|
||||||
|
self.fields['password_%i' % ix] = forms.CharField(label=_("Password"), required=False,
|
||||||
|
widget=forms.TextInput(attrs={'size':'120'}))
|
||||||
|
setattr(self, 'clean_password_%i' % ix, partial(self.clean_password, ix=ix))
|
||||||
self.fields['password1_%i' % ix] = forms.CharField(label=_("Password"),
|
self.fields['password1_%i' % ix] = forms.CharField(label=_("Password"),
|
||||||
widget=forms.PasswordInput, required=False)
|
widget=forms.PasswordInput, required=False)
|
||||||
self.fields['password2_%i' % ix] = forms.CharField(label=_("Password (again)"),
|
self.fields['password2_%i' % ix] = forms.CharField(label=_("Password (again)"),
|
||||||
|
@ -108,23 +117,37 @@ class AdminPasswordChangeForm(forms.Form):
|
||||||
password1 = self.cleaned_data.get('password1%s' % ix)
|
password1 = self.cleaned_data.get('password1%s' % ix)
|
||||||
password2 = self.cleaned_data.get('password2%s' % ix)
|
password2 = self.cleaned_data.get('password2%s' % ix)
|
||||||
if password1 and password2:
|
if password1 and password2:
|
||||||
|
self.password_provided = True
|
||||||
if password1 != password2:
|
if password1 != password2:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
self.error_messages['password_mismatch'],
|
self.error_messages['password_mismatch'],
|
||||||
code='password_mismatch',
|
code='password_mismatch',
|
||||||
)
|
)
|
||||||
elif password1 or password2:
|
elif password1 or password2:
|
||||||
|
self.password_provided = True
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
self.error_messages['password_mismatch'],
|
self.error_messages['password_mismatch'],
|
||||||
code='password_mismatch',
|
code='password_mismatch',
|
||||||
)
|
)
|
||||||
return password2
|
return password2
|
||||||
|
|
||||||
|
def clean_password(self, ix=''):
|
||||||
|
if ix != '':
|
||||||
|
ix = '_%i' % ix
|
||||||
|
password = self.cleaned_data.get('password%s' % ix)
|
||||||
|
if password:
|
||||||
|
self.password_provided = True
|
||||||
|
try:
|
||||||
|
hasher = identify_hasher(password)
|
||||||
|
except ValueError:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
self.error_messages['bad_hash'],
|
||||||
|
code='bad_hash',
|
||||||
|
)
|
||||||
|
return password
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super(AdminPasswordChangeForm, self).clean()
|
if not self.password_provided:
|
||||||
for data in cleaned_data.values():
|
|
||||||
if data:
|
|
||||||
return
|
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
self.error_messages['password_missing'],
|
self.error_messages['password_missing'],
|
||||||
code='password_missing',
|
code='password_missing',
|
||||||
|
@ -134,8 +157,12 @@ class AdminPasswordChangeForm(forms.Form):
|
||||||
"""
|
"""
|
||||||
Saves the new password.
|
Saves the new password.
|
||||||
"""
|
"""
|
||||||
password = self.cleaned_data["password1"]
|
field_name = 'password' if self.raw else 'password1'
|
||||||
|
password = self.cleaned_data[field_name]
|
||||||
if password:
|
if password:
|
||||||
|
if self.raw:
|
||||||
|
self.password = password
|
||||||
|
else:
|
||||||
self.user.set_password(password)
|
self.user.set_password(password)
|
||||||
if commit:
|
if commit:
|
||||||
try:
|
try:
|
||||||
|
@ -144,8 +171,11 @@ class AdminPasswordChangeForm(forms.Form):
|
||||||
# password is not a field but an attribute
|
# password is not a field but an attribute
|
||||||
self.user.save() # Trigger the backend
|
self.user.save() # Trigger the backend
|
||||||
for ix, rel in enumerate(self.related):
|
for ix, rel in enumerate(self.related):
|
||||||
password = self.cleaned_data['password1_%s' % ix]
|
password = self.cleaned_data['%s_%s' % (field_name, ix)]
|
||||||
if password:
|
if password:
|
||||||
|
if raw:
|
||||||
|
rel.password = password
|
||||||
|
else:
|
||||||
set_password = getattr(rel, 'set_password')
|
set_password = getattr(rel, 'set_password')
|
||||||
set_password(password)
|
set_password(password)
|
||||||
if commit:
|
if commit:
|
||||||
|
@ -153,7 +183,7 @@ class AdminPasswordChangeForm(forms.Form):
|
||||||
return self.user
|
return self.user
|
||||||
|
|
||||||
def _get_changed_data(self):
|
def _get_changed_data(self):
|
||||||
data = super(AdminPasswordChangeForm, self).changed_data
|
data = super().changed_data
|
||||||
for name in self.fields.keys():
|
for name in self.fields.keys():
|
||||||
if name not in data:
|
if name not in data:
|
||||||
return []
|
return []
|
||||||
|
@ -173,7 +203,7 @@ class SendEmailForm(forms.Form):
|
||||||
widget=forms.Textarea(attrs={'cols': 118, 'rows': 15}))
|
widget=forms.Textarea(attrs={'cols': 118, 'rows': 15}))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SendEmailForm, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
initial = kwargs.get('initial')
|
initial = kwargs.get('initial')
|
||||||
if 'to' in initial:
|
if 'to' in initial:
|
||||||
self.fields['to'].widget = SpanWidget(original=initial['to'])
|
self.fields['to'].widget = SpanWidget(original=initial['to'])
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.contrib.admin.options import IS_POPUP_VAR
|
||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.contrib.auth import update_session_auth_hash
|
from django.contrib.auth import update_session_auth_hash
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponseRedirect, Http404
|
from django.http import HttpResponseRedirect, Http404, HttpResponse
|
||||||
from django.forms.models import BaseInlineFormSet
|
from django.forms.models import BaseInlineFormSet
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
|
@ -17,9 +17,12 @@ from django.utils.html import escape
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views.decorators.debug import sensitive_post_parameters
|
from django.views.decorators.debug import sensitive_post_parameters
|
||||||
|
|
||||||
|
from orchestra.models.utils import has_db_field
|
||||||
|
|
||||||
from ..utils.python import random_ascii, pairwise
|
from ..utils.python import random_ascii, pairwise
|
||||||
|
|
||||||
from .forms import AdminPasswordChangeForm
|
from .forms import AdminPasswordChangeForm
|
||||||
|
#, AdminRawPasswordChangeForm
|
||||||
#from django.contrib.auth.forms import AdminPasswordChangeForm
|
#from django.contrib.auth.forms import AdminPasswordChangeForm
|
||||||
from .utils import action_to_view
|
from .utils import action_to_view
|
||||||
|
|
||||||
|
@ -240,8 +243,11 @@ class ChangePasswordAdminMixin(object):
|
||||||
return [
|
return [
|
||||||
url(r'^(\d+)/password/$',
|
url(r'^(\d+)/password/$',
|
||||||
self.admin_site.admin_view(self.change_password),
|
self.admin_site.admin_view(self.change_password),
|
||||||
name='%s_%s_change_password' % info)
|
name='%s_%s_change_password' % info),
|
||||||
] + super(ChangePasswordAdminMixin, self).get_urls()
|
url(r'^(\d+)/hash/$',
|
||||||
|
self.admin_site.admin_view(self.show_hash),
|
||||||
|
name='%s_%s_show_hash' % info)
|
||||||
|
] + super().get_urls()
|
||||||
|
|
||||||
def get_change_password_username(self, obj):
|
def get_change_password_username(self, obj):
|
||||||
return str(obj)
|
return str(obj)
|
||||||
|
@ -252,7 +258,10 @@ class ChangePasswordAdminMixin(object):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
# TODO use this insetad of self.get_object(), in other places
|
# TODO use this insetad of self.get_object(), in other places
|
||||||
obj = get_object_or_404(self.get_queryset(request), pk=id)
|
obj = get_object_or_404(self.get_queryset(request), pk=id)
|
||||||
|
raw = request.GET.get('raw', '0') == '1'
|
||||||
|
can_raw = has_db_field(obj, 'password')
|
||||||
|
if raw and not can_raw:
|
||||||
|
raise TypeError("%s has no password db field for raw password edditing." % obj)
|
||||||
related = []
|
related = []
|
||||||
for obj_name_attr in ('username', 'name', 'hostname'):
|
for obj_name_attr in ('username', 'name', 'hostname'):
|
||||||
try:
|
try:
|
||||||
|
@ -268,12 +277,12 @@ class ChangePasswordAdminMixin(object):
|
||||||
else:
|
else:
|
||||||
account = obj
|
account = obj
|
||||||
if account.username == obj_name:
|
if account.username == obj_name:
|
||||||
for rel in account.get_related_passwords():
|
for rel in account.get_related_passwords(db_field=raw):
|
||||||
if not isinstance(obj, type(rel)):
|
if not isinstance(obj, type(rel)):
|
||||||
related.append(rel)
|
related.append(rel)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = self.change_password_form(obj, request.POST, related=related)
|
form = self.change_password_form(obj, request.POST, related=related, raw=raw)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
change_message = self.construct_change_message(request, form, None)
|
change_message = self.construct_change_message(request, form, None)
|
||||||
|
@ -283,18 +292,18 @@ class ChangePasswordAdminMixin(object):
|
||||||
update_session_auth_hash(request, form.user) # This is safe
|
update_session_auth_hash(request, form.user) # This is safe
|
||||||
return HttpResponseRedirect('..')
|
return HttpResponseRedirect('..')
|
||||||
else:
|
else:
|
||||||
form = self.change_password_form(obj, related=related)
|
form = self.change_password_form(obj, related=related, raw=raw)
|
||||||
|
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(obj._meta.verbose_name.capitalize(), {
|
(obj._meta.verbose_name.capitalize(), {
|
||||||
'classes': ('wide',),
|
'classes': ('wide',),
|
||||||
'fields': ('password1', 'password2')
|
'fields': ('password',) if raw else ('password1', 'password2'),
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
for ix, rel in enumerate(related):
|
for ix, rel in enumerate(related):
|
||||||
fieldsets.append((rel._meta.verbose_name.capitalize(), {
|
fieldsets.append((rel._meta.verbose_name.capitalize(), {
|
||||||
'classes': ('wide',),
|
'classes': ('wide',),
|
||||||
'fields': ('password1_%i' % ix, 'password2_%i' % ix)
|
'fields': ('password_%i' % ix,) if raw else ('password1_%i' % ix, 'password2_%i' % ix)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
obj_username = self.get_change_password_username(obj)
|
obj_username = self.get_change_password_username(obj)
|
||||||
|
@ -302,6 +311,8 @@ class ChangePasswordAdminMixin(object):
|
||||||
context = {
|
context = {
|
||||||
'title': _('Change password: %s') % obj_username,
|
'title': _('Change password: %s') % obj_username,
|
||||||
'adminform': adminForm,
|
'adminform': adminForm,
|
||||||
|
'raw': raw,
|
||||||
|
'can_raw': can_raw,
|
||||||
'errors': admin.helpers.AdminErrorList(form, []),
|
'errors': admin.helpers.AdminErrorList(form, []),
|
||||||
'form_url': form_url,
|
'form_url': form_url,
|
||||||
'is_popup': (IS_POPUP_VAR in request.POST or
|
'is_popup': (IS_POPUP_VAR in request.POST or
|
||||||
|
@ -322,3 +333,9 @@ class ChangePasswordAdminMixin(object):
|
||||||
return TemplateResponse(request,
|
return TemplateResponse(request,
|
||||||
self.change_user_password_template,
|
self.change_user_password_template,
|
||||||
context, current_app=self.admin_site.name)
|
context, current_app=self.admin_site.name)
|
||||||
|
|
||||||
|
def show_hash(self, request, id):
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
raise PermissionDenied
|
||||||
|
obj = get_object_or_404(self.get_queryset(request), pk=id)
|
||||||
|
return HttpResponse(obj.password)
|
||||||
|
|
|
@ -246,3 +246,8 @@ PASSLIB_CONFIG = (
|
||||||
"superuser__django_pbkdf2_sha256__default_rounds = 15000\n"
|
"superuser__django_pbkdf2_sha256__default_rounds = 15000\n"
|
||||||
"superuser__sha512_crypt__default_rounds = 120000\n"
|
"superuser__sha512_crypt__default_rounds = 120000\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SHELL_PLUS_PRE_IMPORTS = (
|
||||||
|
('orchestra.contrib.orchestration.managers', ('orchestrate',)),
|
||||||
|
)
|
||||||
|
|
|
@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
#from orchestra.contrib.orchestration.middlewares import OperationsMiddleware
|
#from orchestra.contrib.orchestration.middlewares import OperationsMiddleware
|
||||||
#from orchestra.contrib.orchestration import Operation
|
#from orchestra.contrib.orchestration import Operation
|
||||||
from orchestra.core import services
|
from orchestra.core import services
|
||||||
|
from orchestra.models.utils import has_db_field
|
||||||
from orchestra.utils.mail import send_email_template
|
from orchestra.utils.mail import send_email_template
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
@ -158,7 +159,7 @@ class Account(auth.AbstractBaseUser):
|
||||||
return True
|
return True
|
||||||
return auth._user_has_module_perms(self, app_label)
|
return auth._user_has_module_perms(self, app_label)
|
||||||
|
|
||||||
def get_related_passwords(self):
|
def get_related_passwords(self, db_field=False):
|
||||||
related = [
|
related = [
|
||||||
self.main_systemuser,
|
self.main_systemuser,
|
||||||
]
|
]
|
||||||
|
@ -173,5 +174,8 @@ class Account(auth.AbstractBaseUser):
|
||||||
rel = model.objects.get(account=self, **kwargs)
|
rel = model.objects.get(account=self, **kwargs)
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
continue
|
continue
|
||||||
|
if db_field:
|
||||||
|
if not has_db_field(rel, 'password'):
|
||||||
|
continue
|
||||||
related.append(rel)
|
related.append(rel)
|
||||||
return related
|
return related
|
||||||
|
|
|
@ -112,7 +112,8 @@ class DatabaseUserChangeForm(forms.ModelForm):
|
||||||
password = ReadOnlySQLPasswordHashField(label=_("Password"),
|
password = ReadOnlySQLPasswordHashField(label=_("Password"),
|
||||||
help_text=_("Raw passwords are not stored, so there is no way to see "
|
help_text=_("Raw passwords are not stored, so there is no way to see "
|
||||||
"this user's password, but you can change the password "
|
"this user's password, but you can change the password "
|
||||||
"using <a href=\"../password/\">this form</a>."))
|
"using <a href='../password/'>this form</a>. "
|
||||||
|
"<a onclick='return showAddAnotherPopup(this);' href='../hash/'>Show hash</a>."))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DatabaseUser
|
model = DatabaseUser
|
||||||
|
|
|
@ -3,7 +3,6 @@ from django.shortcuts import redirect
|
||||||
|
|
||||||
|
|
||||||
def last(modeladmin, request, queryset):
|
def last(modeladmin, request, queryset):
|
||||||
last_id = queryset.order_by('id').values_list('id', flat=True).first()
|
last = queryset.model.objects.latest('id')
|
||||||
url = reverse('admin:mailer_message_change', args=(last_id,))
|
url = reverse('admin:mailer_message_change', args=(last.pk,))
|
||||||
print(url)
|
|
||||||
return redirect(url)
|
return redirect(url)
|
||||||
|
|
|
@ -103,7 +103,8 @@ def get_backend_url(ids):
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def message_user(request, logs):
|
def get_messages(logs):
|
||||||
|
messages = []
|
||||||
total, successes, async = 0, 0, 0
|
total, successes, async = 0, 0, 0
|
||||||
ids = []
|
ids = []
|
||||||
async_ids = []
|
async_ids = []
|
||||||
|
@ -140,7 +141,7 @@ def message_user(request, logs):
|
||||||
msg += ', ' + str(async_msg)
|
msg += ', ' + str(async_msg)
|
||||||
msg = msg.format(errors=errors, async=async, async_url=async_url, total=total, url=url,
|
msg = msg.format(errors=errors, async=async, async_url=async_url, total=total, url=url,
|
||||||
name=log.backend)
|
name=log.backend)
|
||||||
messages.error(request, mark_safe(msg + '.'))
|
messages.append(('error', msg + '.'))
|
||||||
elif successes:
|
elif successes:
|
||||||
if async_msg:
|
if async_msg:
|
||||||
if total == 1:
|
if total == 1:
|
||||||
|
@ -160,7 +161,13 @@ def message_user(request, logs):
|
||||||
total=total, url=url, async_url=async_url, async=async, successes=successes,
|
total=total, url=url, async_url=async_url, async=async, successes=successes,
|
||||||
name=log.backend
|
name=log.backend
|
||||||
)
|
)
|
||||||
messages.success(request, mark_safe(msg + '.'))
|
messages.append(('success', msg + '.'))
|
||||||
else:
|
else:
|
||||||
msg = async_msg.format(url=url, async_url=async_url, async=async, name=log.backend)
|
msg = async_msg.format(url=url, async_url=async_url, async=async, name=log.backend)
|
||||||
messages.success(request, mark_safe(msg + '.'))
|
messages.append(('success', msg + '.'))
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
def message_user(request, logs):
|
||||||
|
for func, msg in get_messages(logs):
|
||||||
|
getattr(messages, func)(request, mark_safe(msg))
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import sys
|
||||||
|
from threading import local
|
||||||
|
|
||||||
|
from django.contrib.admin.models import LogEntry
|
||||||
|
from django.db.models.signals import pre_delete, post_save, m2m_changed
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils.decorators import ContextDecorator
|
||||||
|
|
||||||
|
from orchestra.utils.python import OrderedSet
|
||||||
|
|
||||||
|
from . import manager, Operation, helpers
|
||||||
|
from .middlewares import OperationsMiddleware
|
||||||
|
from .models import BackendLog, BackendOperation
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, dispatch_uid='orchestration.post_save_manager_collector')
|
||||||
|
def post_save_collector(sender, *args, **kwargs):
|
||||||
|
if sender not in (BackendLog, BackendOperation, LogEntry):
|
||||||
|
instance = kwargs.get('instance')
|
||||||
|
orchestrate.collect(Operation.SAVE, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_delete, dispatch_uid='orchestration.pre_delete_manager_collector')
|
||||||
|
def pre_delete_collector(sender, *args, **kwargs):
|
||||||
|
if sender not in (BackendLog, BackendOperation, LogEntry):
|
||||||
|
orchestrate.collect(Operation.DELETE, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(m2m_changed, dispatch_uid='orchestration.m2m_manager_collector')
|
||||||
|
def m2m_collector(sender, *args, **kwargs):
|
||||||
|
# m2m relations without intermediary models are shit. Model.post_save is not sent and
|
||||||
|
# by the time related.post_save is sent rel objects are not accessible via RelatedManager.all()
|
||||||
|
if kwargs.pop('action') == 'post_add' and kwargs['pk_set']:
|
||||||
|
orchestrate.collect(Operation.SAVE, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class orchestrate(ContextDecorator):
|
||||||
|
thread_locals = local()
|
||||||
|
thread_locals.pending_operations = None
|
||||||
|
thread_locals.route_cache = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def collect(cls, action, **kwargs):
|
||||||
|
""" Collects all pending operations derived from model signals """
|
||||||
|
if cls.thread_locals.pending_operations is None:
|
||||||
|
# No active orchestrate context manager
|
||||||
|
return
|
||||||
|
kwargs['operations'] = cls.thread_locals.pending_operations
|
||||||
|
kwargs['route_cache'] = cls.thread_locals.route_cache
|
||||||
|
instance = kwargs.pop('instance')
|
||||||
|
manager.collect(instance, action, **kwargs)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
cls = type(self)
|
||||||
|
self.old_pending_operations = cls.thread_locals.pending_operations
|
||||||
|
cls.thread_locals.pending_operations = OrderedSet()
|
||||||
|
self.old_route_cache = cls.thread_locals.route_cache
|
||||||
|
cls.thread_locals.route_cache = {}
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
cls = type(self)
|
||||||
|
if not exc_type:
|
||||||
|
operations = cls.thread_locals.pending_operations
|
||||||
|
if operations:
|
||||||
|
scripts, serialize = manager.generate(operations)
|
||||||
|
logs = manager.execute(scripts, serialize=serialize)
|
||||||
|
for t, msg in helpers.get_messages(logs):
|
||||||
|
if t == 'error':
|
||||||
|
sys.stderr.write('%s: %s\n' % (t, msg))
|
||||||
|
else:
|
||||||
|
sys.stdout.write('%s: %s\n' % (t, msg))
|
||||||
|
cls.thread_locals.pending_operations = self.old_pending_operations
|
||||||
|
cls.thread_locals.route_cache = self.old_route_cache
|
|
@ -11,19 +11,19 @@ from orchestra.utils.python import OrderedSet
|
||||||
|
|
||||||
from . import manager, Operation
|
from . import manager, Operation
|
||||||
from .helpers import message_user
|
from .helpers import message_user
|
||||||
from .models import BackendLog
|
from .models import BackendLog, BackendOperation
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, dispatch_uid='orchestration.post_save_collector')
|
@receiver(post_save, dispatch_uid='orchestration.post_save_collector')
|
||||||
def post_save_collector(sender, *args, **kwargs):
|
def post_save_collector(sender, *args, **kwargs):
|
||||||
if sender not in (BackendLog, Operation, LogEntry):
|
if sender not in (BackendLog, BackendOperation, LogEntry):
|
||||||
instance = kwargs.get('instance')
|
instance = kwargs.get('instance')
|
||||||
OperationsMiddleware.collect(Operation.SAVE, **kwargs)
|
OperationsMiddleware.collect(Operation.SAVE, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, dispatch_uid='orchestration.pre_delete_collector')
|
@receiver(pre_delete, dispatch_uid='orchestration.pre_delete_collector')
|
||||||
def pre_delete_collector(sender, *args, **kwargs):
|
def pre_delete_collector(sender, *args, **kwargs):
|
||||||
if sender not in (BackendLog, Operation, LogEntry):
|
if sender not in (BackendLog, BackendOperation, LogEntry):
|
||||||
OperationsMiddleware.collect(Operation.DELETE, **kwargs)
|
OperationsMiddleware.collect(Operation.DELETE, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -213,6 +213,11 @@ def reissue(modeladmin, request, queryset):
|
||||||
messages.error(request, _("One transaction should be selected."))
|
messages.error(request, _("One transaction should be selected."))
|
||||||
return
|
return
|
||||||
trans = queryset[0]
|
trans = queryset[0]
|
||||||
|
if trans.state != trans.REJECTED:
|
||||||
|
messages.error(request,
|
||||||
|
_("Only rejected transactions can be reissued, "
|
||||||
|
"please reject current transaction if necessary."))
|
||||||
|
return
|
||||||
url = reverse('admin:payments_transaction_add')
|
url = reverse('admin:payments_transaction_add')
|
||||||
url += '?account=%i&bill=%i&source=%s&amount=%s¤cy=%s' % (
|
url += '?account=%i&bill=%i&source=%s&amount=%s¤cy=%s' % (
|
||||||
trans.bill.account_id,
|
trans.bill.account_id,
|
||||||
|
|
|
@ -99,7 +99,6 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
'display_state',
|
'display_state',
|
||||||
'amount',
|
'amount',
|
||||||
'currency',
|
'currency',
|
||||||
'process'
|
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -62,7 +62,8 @@ class UserChangeForm(forms.ModelForm):
|
||||||
password = auth_forms.ReadOnlyPasswordHashField(label=_("Password"),
|
password = auth_forms.ReadOnlyPasswordHashField(label=_("Password"),
|
||||||
help_text=_("Raw passwords are not stored, so there is no way to see "
|
help_text=_("Raw passwords are not stored, so there is no way to see "
|
||||||
"this user's password, but you can change it by "
|
"this user's password, but you can change it by "
|
||||||
"using <a href=\"../password/\">this form</a>."))
|
"using <a href='../password/'>this form</a>. "
|
||||||
|
"<a onclick='return showAddAnotherPopup(this);' href='../hash/'>Show hash</a>."))
|
||||||
|
|
||||||
def clean_password(self):
|
def clean_password(self):
|
||||||
# Regardless of what the user provides, return the initial value.
|
# Regardless of what the user provides, return the initial value.
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
|
@ -16,6 +17,14 @@ def get_model(label, import_module=True):
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def has_db_field(obj, field_name):
|
||||||
|
try:
|
||||||
|
obj._meta.get_field(field_name)
|
||||||
|
except FieldDoesNotExist:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_field_value(obj, field_name):
|
def get_field_value(obj, field_name):
|
||||||
names = field_name.split('__')
|
names = field_name.split('__')
|
||||||
rel = getattr(obj, names.pop(0))
|
rel = getattr(obj, names.pop(0))
|
||||||
|
|
|
@ -8,7 +8,13 @@
|
||||||
<div>
|
<div>
|
||||||
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1" />{% endif %}
|
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1" />{% endif %}
|
||||||
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}" />{% endif %}
|
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}" />{% endif %}
|
||||||
<p>{% blocktrans with username=obj_username %}Enter a new password for the user <strong>{{ username }}</strong>, suggestion '{{ password }}'.{% endblocktrans %}</p>
|
<p>
|
||||||
|
{% if raw %}
|
||||||
|
{% blocktrans with username=obj_username %}Enter a new password hash for user <strong>{{ username }}</strong>. Switch to <a href="./?raw=0">text password form</a>.{% endblocktrans %}
|
||||||
|
{% elif can_raw %}
|
||||||
|
{% blocktrans with username=obj_username %}Enter a new password for user <strong>{{ username }}</strong>, suggestion '{{ password }}'. Switch to <a href="./?raw=1">raw password form</a>.{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
|
||||||
{% if errors %}
|
{% if errors %}
|
||||||
<p class="errornote">
|
<p class="errornote">
|
||||||
|
|
Loading…
Reference in New Issue