Django Admin: Prevent initial value from changing - django

I currently have:
def formfield_for_dbfield(self, db_field, request, obj=None, **kwargs):
if db_field.name == "username":
initial_username = obj.username if obj else generate_patient_number()
kwargs["initial"] = initial_username
kwargs["disabled"] = True
kwargs[
"help_text"
] = "<span style='color: red;'>Number might change. Please look at banner once saved</span>"
return super().formfield_for_dbfield(db_field, request, **kwargs)
However, once a new instance is created, the value that was displayed initially changes. Is there a way to prevent this?

Related

Django-Admin : Override add-URL of the Foreign Key Auto-Complete "add" button / + green cross

Is there a simple way of overriding/pass a parameter to the popup url automatically created by django-admin built-in autocomplete/select2 widget to create a new foreign key object? This url is embedded in the green cross (see picture).
I didn't come across any well described solutions.
So the default url is pointing towards admin/app/model/add/?_to_field=id&_popup=1 but I would like to add a parameter admin/app/model/add/?_to_field=id&_popup=1&field_1=100 in order to pre-populate some fieds on the popup add_view.
Any leads?
As per Maxim's answer :
def formfield_for_dbfield(self, db_field, request, **kwargs):
if db_field.name == "clinical_exam":
widget = super(ConsultationAdmin, self).formfield_for_dbfield(db_field, request, **kwargs).widget
widget.template_name = 'admin/related_widget_wrapper_with_id.html'
widget.attrs.update({'your_parameter': 'you can use it after in template'})
print(db_field)
return db_field.formfield(widget=widget)
return super(ConsultationAdmin, self).formfield_for_dbfield(db_field, request, **kwargs)
'change + -' this is a result of render RelatedFieldWidgetWrapper
from django.contrib.admin.widgets
You can change attributes of this widget, but it is a little bit complicated.
in ModelAdmin.formfield_for_dbfield:
def formfield_for_dbfield(self, *args, **kwargs):
widget = super.formfield_for_dbfield(self, *args, **kwargs).widget
if isinstance(widget, RelatedFieldWidgetWrapper):
old_context = widget.get_context
widget.get_context = my_function(widget.get_context, *args, **kwargs)
my function is a decorator for old function:
def my_function(func, *initargs, **initkwargs):
def wrapped(*args, **kwargs):
context = func(*args, **kwargs)
your_keys_vals_to_add_in_url = 'something in form key=val&'
context['url_params'] = f'{context['url_params']}&{your_keys_vals_to_add_in_url}'
return context
return wrapped
Other possibility is - to change template related_widget_wrapper.html
from django.admin.contrib.templates.admin.widgets
You can hardcoded your values there.
in ModelAdmin.formfield_for_dbfield:
def formfield_for_dbfield(self, *args, **kwargs):
widget = super.formfield_for_dbfield(self, *args, **kwargs).widget
if isinstance(widget, RelatedFieldWidgetWrapper):
widget.template_name = 'path/to/your_overriden_related_widget_wrapper.html'
widget.attrs.update('your_parameter' : 'you can use it after in template') # this is not work right now in django
and after it in own template:
# example for change link
# this is not work right now in django
<a ... data-href-template="{{ change_related_template_url }}?{{ url_params }}&{{ attr.your_parameter|dafault:'' }}" .. >
The last part not works in Django right now. I made an issue in Django project about this possibility.

Django Two Factor Authentication - Remove Token Generator Option From Wizard

I've been playing with Django Two Factor authentication for the last two days or so, and I have it partially working. I am trying to figure out a way to remove the QR Token Generator. I have tried subclassing the setup view, but the form wizard is causing me some grief. The wizard is confusing me. I know in a regular form, how to remove radio buttons, but in this case, I can't seem to locate the source of the Token Generator.
The SetupView...
#class_view_decorator(never_cache)
#class_view_decorator(login_required)
class SetupView(IdempotentSessionWizardView):
"""
View for handling OTP setup using a wizard.
The first step of the wizard shows an introduction text, explaining how OTP
works and why it should be enabled. The user has to select the verification
method (generator / call / sms) in the second step. Depending on the method
selected, the third step configures the device. For the generator method, a
QR code is shown which can be scanned using a mobile phone app and the user
is asked to provide a generated token. For call and sms methods, the user
provides the phone number which is then validated in the final step.
"""
success_url = 'two_factor:setup_complete'
qrcode_url = 'two_factor:qr'
template_name = 'two_factor/core/setup.html'
session_key_name = 'django_two_factor-qr_secret_key'
initial_dict = {}
form_list = (
('welcome', Form),
('method', MethodForm),
('generator', TOTPDeviceForm),
('sms', PhoneNumberForm),
('call', PhoneNumberForm),
('validation', DeviceValidationForm),
('yubikey', YubiKeyDeviceForm),
)
condition_dict = {
'generator': lambda self: self.get_method() == 'generator',
'call': lambda self: self.get_method() == 'call',
'sms': lambda self: self.get_method() == 'sms',
'validation': lambda self: self.get_method() in ('sms', 'call'),
'yubikey': lambda self: self.get_method() == 'yubikey',
}
idempotent_dict = {
'yubikey': False,
}
def get_method(self):
method_data = self.storage.validated_step_data.get('method', {})
return method_data.get('method', None)
def get(self, request, *args, **kwargs):
"""
Start the setup wizard. Redirect if already enabled.
"""
if default_device(self.request.user):
return redirect(self.success_url)
return super(SetupView, self).get(request, *args, **kwargs)
def get_form_list(self):
"""
Check if there is only one method, then skip the MethodForm from form_list
"""
form_list = super(SetupView, self).get_form_list()
available_methods = get_available_methods()
if len(available_methods) == 1:
form_list.pop('method', None)
method_key, _ = available_methods[0]
self.storage.validated_step_data['method'] = {'method': method_key}
return form_list
def render_next_step(self, form, **kwargs):
"""
In the validation step, ask the device to generate a challenge.
"""
next_step = self.steps.next
if next_step == 'validation':
try:
self.get_device().generate_challenge()
kwargs["challenge_succeeded"] = True
except Exception:
logger.exception("Could not generate challenge")
kwargs["challenge_succeeded"] = False
return super(SetupView, self).render_next_step(form, **kwargs)
def done(self, form_list, **kwargs):
"""
Finish the wizard. Save all forms and redirect.
"""
# Remove secret key used for QR code generation
try:
del self.request.session[self.session_key_name]
except KeyError:
pass
# TOTPDeviceForm
if self.get_method() == 'generator':
form = [form for form in form_list if isinstance(form, TOTPDeviceForm)][0]
device = form.save()
# PhoneNumberForm / YubiKeyDeviceForm
elif self.get_method() in ('call', 'sms', 'yubikey'):
device = self.get_device()
device.save()
else:
raise NotImplementedError("Unknown method '%s'" % self.get_method())
django_otp.login(self.request, device)
return redirect(self.success_url)
def get_form_kwargs(self, step=None):
kwargs = {}
if step == 'generator':
kwargs.update({
'key': self.get_key(step),
'user': self.request.user,
})
if step in ('validation', 'yubikey'):
kwargs.update({
'device': self.get_device()
})
metadata = self.get_form_metadata(step)
if metadata:
kwargs.update({
'metadata': metadata,
})
return kwargs
def get_device(self, **kwargs):
"""
Uses the data from the setup step and generated key to recreate device.
Only used for call / sms -- generator uses other procedure.
"""
method = self.get_method()
kwargs = kwargs or {}
kwargs['name'] = 'default'
kwargs['user'] = self.request.user
if method in ('call', 'sms'):
kwargs['method'] = method
kwargs['number'] = self.storage.validated_step_data\
.get(method, {}).get('number')
return PhoneDevice(key=self.get_key(method), **kwargs)
if method == 'yubikey':
kwargs['public_id'] = self.storage.validated_step_data\
.get('yubikey', {}).get('token', '')[:-32]
try:
kwargs['service'] = ValidationService.objects.get(name='default')
except ValidationService.DoesNotExist:
raise KeyError("No ValidationService found with name 'default'")
except ValidationService.MultipleObjectsReturned:
raise KeyError("Multiple ValidationService found with name 'default'")
return RemoteYubikeyDevice(**kwargs)
def get_key(self, step):
self.storage.extra_data.setdefault('keys', {})
if step in self.storage.extra_data['keys']:
return self.storage.extra_data['keys'].get(step)
key = random_hex(20).decode('ascii')
self.storage.extra_data['keys'][step] = key
return key
def get_context_data(self, form, **kwargs):
context = super(SetupView, self).get_context_data(form, **kwargs)
if self.steps.current == 'generator':
key = self.get_key('generator')
rawkey = unhexlify(key.encode('ascii'))
b32key = b32encode(rawkey).decode('utf-8')
self.request.session[self.session_key_name] = b32key
context.update({
'QR_URL': reverse(self.qrcode_url)
})
elif self.steps.current == 'validation':
context['device'] = self.get_device()
context['cancel_url'] = resolve_url(settings.LOGIN_REDIRECT_URL)
return context
def process_step(self, form):
if hasattr(form, 'metadata'):
self.storage.extra_data.setdefault('forms', {})
self.storage.extra_data['forms'][self.steps.current] = form.metadata
return super(SetupView, self).process_step(form)
def get_form_metadata(self, step):
self.storage.extra_data.setdefault('forms', {})
return self.storage.extra_data['forms'].get(step, None)
Seems to reference a MethodForm...
class MethodForm(forms.Form):
method = forms.ChoiceField(label=_("Method"),
initial='generator',
widget=forms.RadioSelect)
def __init__(self, **kwargs):
super(MethodForm, self).__init__(**kwargs)
self.fields['method'].choices = get_available_methods()
I can't seem to track back to where the list of choices is defined, clearly in this case it is saying generator is the initial choice in setup, but I can't figure out how to remove the generator option as a list of valid choices. I also tried to remove generator from the form_list but this didn't seem to make a difference either.
If there's an easier way to remove the Token Generator option and a different approach that's altogether different, I'm open to that too.
Thanks in advance for any thoughts.
Found it. It was in the models.py....
def get_available_methods():
methods = [('generator', _('Token generator'))]
methods.extend(get_available_phone_methods())
methods.extend(get_available_yubikey_methods())
return methods
I removed the ('generator', _('Token generator')) reference from inside the brackets ( list ) and it removed the Token Generator option.

Customize raw_id field query on an inline form

How do I customize the spyglass query in an inline form for a raw_id foreignkey?
I tried overriding formfield_for_foreignkey but that did nothing and I think it's because it's for dropdown foreignkeys rather than raw_id. I also tried a custom widget but that doesn't seem to work on inline.
So after a lot of digging around, this is what I came up with.
from django.admin import widgets
class ItemSubRecipeRawIdWidget(widgets.ForeignKeyRawIdWidget):
def url_parameters(self):
res = super(ItemSubRecipeRawIdWidget, self).url_parameters()
# DO YOUR CUSTOM FILTERING HERE!
res['active'] = True # here I filter on recipe.active==True
return res
class ItemSubRecipeInline(admin.TabularInline):
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
field = super(ItemSubRecipeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
if db_field.name == 'recipe':
field.widget = ItemSubRecipeRawIdWidget(rel=ItemSubRecipe._meta.get_field('recipe').rel, admin_site=site)
return field
So the spyglass thing is a ForeignKeyRawIdWidget and you need to override the default with a custom one. The url_parameters function on the widget is what is passed to build the query that populates the list of usable object foreignkeys.
Is enought to do following:
class ItemSubRecipeInline(admin.TabularInline):
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
field = super(ItemSubRecipeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
if db_field.name == 'recipe':
field.widget.rel.limit_choices_to = {'your_field_to_filter': True}
return field

Limiting the options of foreign key in ModelAdmin returns "Select a valid choice"

I am attempting to limit the option to a foreign key in the admin app for a specific user (The field that i am trying to limit is called school) . This is what my code looks like - Unfortunately there are two problems (mentioned below) when I attempt to edit a student (by clicking on their name).
1.The default value for school is --
2.When I select the right school from the drop down and attempt to save I get the error on school field saying
Select a valid choice. That choice is not one of the available
choices.
This is what it looks like
class modelStudentAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super(modelStudentAdmin, self).get_queryset(request)
if request.user.is_superuser:
return qs
else:
schoolInstance = modelSchool.objects.get(user=request.user)
qs = modelStudent.objects.filter(school=schoolInstance)
return qs
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if request.user.is_superuser:
return super(modelStudentAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
#Not superuser only staff
if db_field.name == 'school':
t = modelSchool.objects.filter(user=request.user).values_list("school_name",flat=True)
kwargs['queryset'] = t
return super(modelStudentAdmin,self).formfield_for_foreignkey(db_field, request, **kwargs)
Now if I remove the method
def formfield_for_foreignkey(self, db_field, request, **kwargs):
everything works but then I cannot restrict the foreign key. Any suggestions on what I might be doing wrong ?
Try replacing
t = modelSchool.objects.filter(user=request.user).values_list("school_name",flat=True)
with this
modelSchool.objects.filter(user=request.user)
you dont need to value_list your query set.

How do I access a model field's value in this case?

I need to change the widget used in the admin, based on the value of the db_field. Here's where I'm trying to step in:
def formfield_for_dbfield(self,db_field,**kwargs):
field = super(MyAdmin, self).formfield_for_dbfield(db_field, **kwargs)
if db_field.name == "my_custom_name":
# how can I check here the value of the object?
I've been trying various combinations in the shell for the past 10 minutes, to no result.
Ok, so here's how I finally did it:
class MyAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
self.object_instance = obj
return super(MyAdmin,self).get_form(request,obj,**kwargs)
After that, everything was easy.