Validating dynamically created ModelForm field in Django 2 - django

I am using Django 2.0 and I have a model for Articles and a model for Storylines. A storyline contains many related articles.
class Article(models.Model):
headline_text = models.CharField(max_length=255, verbose_name='Headline')
storylines = models.ManyToManyField(Storyline, verbose_name='Add to Storylines')
I have a ModelForm that will allow you to choose an article to add to the Storyline. That ModelForm class looks like this:
class StorylineAddArticleForm(forms.Form):
articleSearchBox = forms.CharField(label="Search to narrow list below:")
include_articles = [article.id for article in Article.objects.order_by('-sub_date')[:5]]
articles = forms.ModelMultipleChoiceField(queryset=Article.objects.filter(id__in=include_articles).order_by('-sub_date'))
def __init__(self, *args, **kwargs):
super(StorylineAddArticleForm, self).__init__(*args, **kwargs)
self.fields['articleSearchBox'].required = False
self.helper = FormHelper(self)
self.helper.layout = Layout(
Field('articleSearchBox'),
Field('articles'),
ButtonHolder(
Submit('submit', 'Add', css_class='button white')
)
)
So far so good, if I submit any article in the queryset, the form validates and saves as needed.
The live site will have many more articles than will be practical to display in the ModelMultipleChoice field, so I do some JQuery to allow the user to use articleSearchBox to replace the ModelMultipleChoice field. This works brilliantly and you can do a search for any article, including those not in the original queryset. Here's that:
{% block content %}
<h2>Add Article</h2>
Add an existing article to <strong>{{ storyline.headline_text }}</strong> storyline:<br>
Did you want to add a new article instead?<br>
<hr>
{% crispy form %}
{% endblock %}
{% block pagescripts %}
<script>
$(document).ready(function(){
$("#id_articleSearchBox").on('input propertychange paste', function(){
$.ajax({
url:'/webproxy/a/?q=' + $("#id_articleSearchBox").val(),
type:'get',
dataType:'html',
crossDomain:true,
success:function(data)
{
$("#id_articles").empty().append(data);
},
error: function(data) {
$("#id_articles").empty().append("<option value=\"-1\">No results</option>");
}
});
}); // end article search box
});
</script>
{% endblock %}
THE PROBLEM:
If I do a search and get an article that was not in the original queryset, the validation fails and I am told that it is not a valid choice. I need a validator that will allow any article or articles, as long as they are actually in the database.
WHAT I HAVE TRIED:
I tried creating a validator that looks like this:
def clean_article(self):
art_ID = self.cleaned_data.get('articles', False)
if(art_ID):
try:
art = Article.objects.get(pk=art_ID)
except ObjectDoesNotExist:
return None
else:
return None
# if we are here, we have an article.
return art
This produced no change in behavior. I have looked and looked for a validator that would even allow any value or just check if it exists, but I am not having a lot of luck.

Your custom validator doesn't have any effect as it will be called after the field's validation. For more information about the order validations are run in refer to the django docs.
What you can do instead is overriding said field validation by inheriting from Django's MultipleChoiceField:
from django import forms
class ArticleMultipleChoiceField(forms.MultipleChoiceField):
def validate(self, value):
pass # your custom validation
You will then of course have to use your custom ArticleMultipleChoiceField in your StorylineAddArticleForm for the articles field.

Related

Django Admin: Add Hyperlink to related model

I would like to add a hyperlink to the related model Training
It would be nice to have declarative solution, since I want to use
this at several places.
The "pencil" icon opens the related model in a popup window. That's not what I want. I want a plain hyperlink to the related model.
BTW, if you use "raw_id_fields", then the result is exactly what I was looking for: There is a hyperlink to the corresponding admin interface of this ForeignKey.
Update Jan 4, 2023
From Django 4.1, this becomes a part of the official build (related PR).
Related widget wrappers now have a link to object’s change form
Result
Previous Answer
The class named RelatedFieldWidgetWrapper is showing the icons on the Django Admin page and thus you need to override the same. So, create a custom class as below,
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
class CustomRelatedFieldWidgetWrapper(RelatedFieldWidgetWrapper):
template_name = 'admin/widgets/custom_related_widget_wrapper.html'
#classmethod
def create_from_root(cls, root_widget: RelatedFieldWidgetWrapper):
# You don't need this method of you are using the MonkeyPatch method
set_attr_fields = [
"widget", "rel", "admin_site", "can_add_related", "can_change_related",
"can_delete_related", "can_view_related"
]
init_args = {field: getattr(root_widget, field) for field in set_attr_fields}
return CustomRelatedFieldWidgetWrapper(**init_args)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
rel_opts = self.rel.model._meta
info = (rel_opts.app_label, rel_opts.model_name)
context['list_related_url'] = self.get_related_url(info, 'changelist')
return context
See, the context variable list_related_url is the relative path that we need here. Now, create an HTML file to render the output,
#File: any_registered_appname/templates/admin/widgets/custom_related_widget_wrapper.html
{% extends "admin/widgets/related_widget_wrapper.html" %}
{% block links %}
{{ block.super }}
- Link To Related Model -
{% endblock %}
How to connect?
Method-1 : Monkey Patch
# admin.py
# other imports
from ..widgets import CustomRelatedFieldWidgetWrapper
from django.contrib.admin import widgets
widgets.RelatedFieldWidgetWrapper = CustomRelatedFieldWidgetWrapper # monket patch
Method-2 : Override ModelAdmin
# admin.py
class AlbumAdmin(admin.ModelAdmin):
hyperlink_fields = ["related_field_1"]
def formfield_for_dbfield(self, db_field, request, **kwargs):
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
if db_field.name in self.hyperlink_fields:
formfield.widget = CustomRelatedFieldWidgetWrapper.create_from_root(
formfield.widget
)
return formfield
Result
There are several ways to go. Here is one.
Add some javascript that changes the existing link behavior. Add the following script at the end of the overridden admin template admin/widgets/related_widget_wrapper.html. It removes the class which triggers the modal and changes the link to the object.
It will only be triggered for id_company field. Change to your needs.
{% block javascript %}
<script>
'use strict';
{
const $ = django.jQuery;
function changeEditButton() {
const edit_btn = document.getElementById('change_id_company');
const value = edit_btn.previousElementSibling.value;
const split_link_template = edit_btn.getAttribute('data-href-template').split('?');
edit_btn.classList.remove('related-widget-wrapper-link');
edit_btn.setAttribute('href', split_link_template[0].replace('__fk__', value));
};
$(document).ready(function() {
changeEditButton();
$('body').on('change', '#id_company', function(e) {
changeEditButton();
});
});
}
</script>
{% endblock %}
This code can also be modified to be triggered for all edit buttons and not only for the company edit button.

How to re-display formset & Select2 field with selected value on form error in Django

After searching for several days and trying different options, I decided to finally post the issue and question.
I have a template that has a form and 2 different formsets.
One of the formsets uses an intermediate model with a GenericForeignKey that will reference two other models.
For the formset, I am using an inlineformset and adding a CharField which is used with Select2 to make an ajax call to check the two other models. The value returned by the ajax call will be a json/dict with 3 key/value pairs.
The issue I am having is that when the template is submitted and there are errors, how can I redisplay the value that was entered in the Select2 CharField when the template is presented again?
The value is in self.data and is sent back to the template.
However, everything I've tried so far will not redisplay the select2 field with the value selected previously or the values that were submitted.
The submitted values are returned to the template in a json/dict, key/value, format under form.fieldname.value but I am not sure how I can use that to repopulate the select2 field.
I appreciate any suggestions or links. If there is an alternate way to set this up, I am interested to hear.
Thank you.
UPDATE: 2021-03-18
Here is, hopefully all, the relevant bits from the various files.
models.py
class SiteDomain(models.Model):
website = models.ForeignKey(
WebSite,
on_delete=models.CASCADE,
)
domain_model = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
help_text=(
"The model that the website entry is related to. eg: Domain or SubDomain"
),
)
object_id = models.PositiveIntegerField(
help_text="The ID of the model object the entry is related to."
)
content_object = GenericForeignKey("domain_model", "object_id")
content_object.short_description = "Domain Name:"
views.py
class AddWebsite(View):
def get(self, request, *args, **kwargs):
domain_formset = inlineformset_factory(
WebSite,
SiteDomain,
formset=SiteDomainInlineFormSet,
fields=(),
extra=3,
)
forms.py
class SiteDomainInlineFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
self.account = kwargs.pop('account', None)
super(SiteDomainInlineFormSet, self).__init__(*args, **kwargs)
def add_fields(self, form, index):
super().add_fields(form, index)
form.fields["domain_name"] = forms.CharField(
max_length=255,
widget=forms.Select(),
required=False,
)
template
<script type="text/javascript">
function s2search() {
$('.domain-lookup-ajax').select2({
width: 'style',
ajax: {
url: "{% url 'accounts_ajax:website_domain_lookup' %}",
dataType: 'json',
delay: 250,
data: function (params) {
var query = {
term: params.term,
acct_id: '{{ account.id }}',
}
return query;
},
processResults: function (data, params) {
return {
results: data,
};
},
cache: true
},
placeholder: 'Enter at least 2 characters for search.',
minimumInputLength: 2,
});
}
</script>
<form action="" method="post">
{% csrf_token %}
{{ domain_formset.management_form }}
{{ app_formset.management_form }}
{% for form in domain_formset %}
<div class="domainfieldWrapper" id="row_{{ forloop.counter0 }}">
<select id="id_dform-{{ forloop.counter0 }}-domain_name" class="domain_name domain-lookup-ajax" name="dform-{{ forloop.counter0 }}-domain_name"></select>
<button id="id_dform-{{ forloop.counter0 }}-button" class="button" type="button" onclick="clearSelect('id_dform-{{ forloop.counter0 }}-domain_name')">Clear</button>
</div>
{% endfor %}
</form>
The ajax call will return something like:
{"model_id":"74", "domain_id":"177", "name":"alfa.first-example.com"}
A side note:
I also tested the select2 field in the second formset and it does not get repopulated either when the template is reloaded if there are any form errors. Which I kind of expected since it basically uses the same setup except for the value returned by the ajax call which is for a normal ModelChoiceField.
Using a combination of https://select2.org/programmatic-control/add-select-clear-items#preselecting-options-in-an-remotely-sourced-ajax-select2, js and Django template tags I was able to get something that works for me.

How to integrate a form into a Detail View?

I would like to do:
I am trying to create a form input on a detail view that will update a particular data column ('status') of the detailed model instance. Here is a picture of what I have in mind:
The selector would display the current status and the user could change it and update from the detail view without having to access the UpdateView.
my idea here would be to have this happen:
1. On submit, get the new user entered value.
2. get the model instance of the currently detailed class
3. assign the model instance attribute as the user entered value
4. save the model instance
I've tried: I don't know if this is the best way to do this but i've been trying to create an AJAX call, mostly by looking for examples online.
Results: Terminal shows Post on submit: "[19/Nov/2019 17:50:33] "POST /task/edit/4 HTTP/1.1" 200 41256". However, the data is not saved to the db. On refresh, the selector returns to previously saved status.
The console shows: "script is connected", and "Update Status" with no errors. On submit, the alert displays success message: "127.0.0.1:8000 says status updated".
Task_detail.html
<div class="deliv-box edit">
<form id="status-update-form" method='POST' action='{% url "task_edit" task.pk %}'>
{% csrf_token %}
{{task_form.status}}
<input id="status-update-btn" type="submit" value="Update Status" />
</form>
</div>
...
<script type="text/javascript">
var frm = $('#status-update-form');
frm.submit(function () {
console.log("script is connected")
console.log($('#status-update-btn').val())
$.ajax({
type: frm.attr('method'),
url: frm.attr('action'),
data: frm.serialize(),
success: function (data) {
$("#deliv-box edit").html(data);
alert("status updated");
},
error: function(data) {
alert("error");
}
});
return false;
});
</script>
forms.py
class TaskForm(forms.ModelForm):
class Meta:
model = Task
fields = "__all__"
views.py
class TaskDetail(ModelFormMixin, DetailView):
template_name='task_detail.html'
model = Task
form_class = TaskForm
def get_context_data(self, **kwargs):
context = super(TaskDetail, self).get_context_data(**kwargs)
context['task_form'] = self.get_form
return context
def update(request):
if request.method=='POST':
task_id = request.POST.get('id')
task = Task.objects.get(pk = task_id)
status_obj = request.POST.get('status')
task.status = status_obj
task.save()
return JsonResponse({'status':'updated...'})
else:
return JsonResponse({'status':'not updated'})
thank you.
A solution:
In the unlikely event that someone stumbles across this question and who is, like me, just trying to figure it out all by themselves, here is what I've learned about how this works: When a user wants to update a form, Django pre-populates the form with the existing data related to that instance. A user can then alter the data and re-submit the form.
Here, I was attempting to alter just one field of the exiting instance, but as I was only calling that one field, Django was assuming not, as I had hoped, that the other fields would remain the same, but that I intended the other fields to be submitted as blank. Where the fields are required one cannot return that field as blank. Therefore, Django was not able to validate the form and so the form did not get updated.
A solution that works is to call all the fields as hidden and show just the one you want to alter. This way Django can return the unaltered data and validate the form, and you get an update button on your detail view:
<form method="POST">
{% csrf_token %}
<h4> STATUS: </h4>
{% for field in form %}
{{ field.as_hidden }}
{% endfor %}
{{form.status}}
<button type="submit" class="btn btn-success">submit</button>
</form>
You are overriding the method update which does not exist, so it is never called.
You need to subclass UpdateView instead of the DetailView and the mixin.
class TaskUpdateView(UpdateView):
template_name='task_detail.html'
model = Task
form_class = TaskForm
# you can use the line below instead of defining form_class to generate a model form automatically
# fields = ('status', )
def form_valid(self, form):
post = form.save(commit=False)
# do anything here before you commit the save
post.save()
# or instead of two lines above, just do post = form.save()
return JsonResponse({'status':'updated...'})
Here is how you would add readonly (disabled) fields to your form:
class TaskForm(forms.ModelForm):
# override the default form field definitions for readonly fields
other_field = forms.CharField(disabled=True)
another_field = forms.IntegerField(disabled=True)
class Meta:
model = Task
fields = ("status", "other_field", "another_field")
# you could also just do:
# fields = '__all__'

Django: How to add a print button to objects to print some (special) fields of a model instance

(Hopefully) not a duplicate:
I know this might seem to be quite similar to Django admin overriding - adding a print button
But the answer there is to use django-object-actions, which I already tried but it looks a bit too overloaded for such an simple task. Furthermore the buttons there are not placed behind the row.
My question:
I would like to create a printable view of some fields of a Django models instance.
Let's say I want to print an users
Name
Last Name
Number
What I image is something like this:
Clicking on a print button, shown at the list view:
An preformatted and easy to print website opens which contains the data:
What I have so far
I added the button by using the following code:
class MyModelAdmin(admin.ModelAdmin):
list_display = ('number', 'name', 'last_name', ..., 'account_actions')
...
def account_actions(self, obj):
return format_html(
'<form method="post" action="/print_view.htm"> \
<input type="hidden" name="name" value="{}"> \
<button type="submit" name="action">Print</button> \
</form>',
obj.name
)
account_actions.short_description = 'Actions'
account_actions.allow_tags = True
So my idea is to send the data which I want to get printed to another Website (via POST, on the same server). I would extract the data from the request then and create a printable view.
My question is:
Is it possible to do the same within Django (without leaving DjangoAdmin for the printable view)?
The current approach doesn't feel right too me, I bet there is a way to do that using just Django - a way which I don't know of since I am quite a beginner here.
I found a great module out there that is called django-admin-object-actions, it can be found here: https://github.com/ninemoreminutes/django-admin-object-actions
The maintainer/owner #cchurch helped me out with the following answer:
You can specify a custom view method that can render any template or
return any content you'd like. Here's the simplest example I can come
up with to do that:
class TestModelAdmin(ModelAdminObjectActionsMixin, admin.ModelAdmin):
# all of the normal model admin code here
object_actions = [
{
'slug': 'print',
'verbose_name': _('Print'),
'form_method': 'GET',
'view': 'print_view',
},
]
def print_view(self, request, object_id, form_url='', extra_context=None, action=None):
from django.template.response import TemplateResponse
obj = self.get_object(request, object_id)
return TemplateResponse(request, 'print.html', {'obj': obj})
Using the following template (print.html):
<p>Name: {{ obj.name }}</p>
<p>Enabled: {{ obj.enabled }}</p>

Django and fieldsets on ModelForm

I know you can specify fieldsets in django for Admin helpers. However, I cannot find anything useful for ModelForms. Just some patches which I cannot use. Am I missing something? Is there a way I could achieve something like fieldsets without manually writing out each field on my template in the appropriate tag.
I would ideally like to iterate through a set of BoundFields. However, doing something like this at the end of my ModelForm:
fieldsets = []
fieldsets.append(('Personal Information',
[username,password,password2,first_name,last_name,email]),) # add a 2 element tuple of string and list of fields
fieldsets.append(('Terms & Conditions',
[acceptterms,acceptprivacy]),) # add a 2 element tuple of string and list of fields
fails as the items contained in my data structure are the raw fields, not the BoundFields. t looks like BoundFields are generated on the fly... this makes me sad. Could I create my own subclass of forms.Form which contains a concept of fieldsets (even a rough one that is not backward compatible... this is just for my own project) and if so, can you give any pointer? I do not want to mess with the django code.
I think this snippet does exactly what you want. It gives you a Form subclass that allows you to declaratively subdivide your form into fieldsets and iterate through them in your template.
Update: that snippet has since become part of django-form-utils
Fieldsets in modelforms are still in "design" stage. There's a ticket in Django trac with low activity.
It's something I've been interested in researching myself in the near future, but since I haven't done it yet the best I can offer are these snippets:
Form splitting/Fieldset templatetag
Sectioned Form
Forms splitted in fieldsets
Edit: I just noticed this question again and I realize it needs an edit to point out Carl's project django-form-utils which contains a BetterForm class which can contain fieldsets. If you like this project give him a +1 for his answer below :)
One thing you can do is break your logical fieldsets into separate model form classes.
class PersonalInfoForm (forms.ModelForm):
class Meta:
model=MyModel
fields=('field1', 'field2', ...)
class TermsForm (forms.ModelForm):
class Meta:
model=MyModel
fields=('fieldX', 'fieldY', ...)
Pass them to your template in different variables and break up the formsets:
<form ...>
<fieldset><legend>Personal Information</legend>
{{ personal_info_form }}
</fieldset>
<fieldset><legend>Terms and Conditions</legend>
{{ terms_form }}
</fieldset>
</form>
In that sense each of your form classes is just a fragment of the actual HTML form.
It introduces a touch of complexity when you call save on the form. You'll probably want to pass commit=False and then merge the resultant objects. Or just avoid using ModelForm.save altogether and populate your model object by hand with 'cleaned_data'
Daniel Greenfelds django-uni-form solves this with a the Layout helper class. I'm trying it out right now and it looks pretty clean to me.
Uniform helpers can use layout objects. A layout can consist of fieldsets, rows, columns, HTML and fields.
I originally picked Django-uni-form because it complies with section 508.
You can use this package: https://pypi.org/project/django-forms-fieldset/
pip install django-forms-fieldset
Add forms_fieldset to your INSTALLED_APPS setting like this:
INSTALLED_APPS = [
...
'forms_fieldset',
]
Add fieldsets in your form
from django.forms import ModelForm
from .models import Student
class StudentForm(ModelForm):
fieldsets = [
("Student Information", {'fields': [
('first_name', 'last_name'),
('email', 'adress'),
]}),
("Parent Information", {'fields': [
'mother_name',
'father_name',
]}),
]
class Meta:
model = Student
fields = '__all__'
In your views
def home(request):
form = StudentForm()
if request.method == 'POST':
form = Form(request.POST, request.FILES)
#save...
context = {
'form': form,
}
return render(request, 'home.html', context)
in your template
{% load forms_fieldset static %}
<link rel="stylesheet" type="text/css" href="{% static 'forms_fieldset/css/main.css' %}">
<form>
{{ form|fieldset:'#42945c' }}
</form>
This was the code that I developed in order to understand custom tags (with links). I applied it to create a fieldset.
Disclaimer: I encourage the use of any of the above answers, this was just for the sake of learning.
templatetags/myextras.py:
from django import template
from django.template import Context
register = template.Library()
class FieldsetNode(template.Node):
""" Fieldset renderer for 'fieldset' tag """
def __init__(self, nodelist, fieldset_name):
""" Initialize renderer class
https://docs.djangoproject.com/en/1.8/howto/custom-template-tags/#writing-the-renderer
:param nodelist: a list of the template nodes inside a block of 'fieldset'
:param fieldset_name: the name of the fieldset
:return: None
"""
self.nodelist = nodelist
self.fieldset_name = fieldset_name
def render(self, context):
""" Render the inside of a fieldset block based on template file
https://docs.djangoproject.com/en/1.8/howto/custom-template-tags/#auto-escaping-considerations
:param context: the previous template context
:return: HTML string
"""
t = context.template.engine.get_template('myapp/fieldset.html')
return t.render(Context({
'var': self.nodelist.render(context),
'name': self.fieldset_name,
}, autoescape=context.autoescape))
#register.tag
def fieldset(parser, token):
""" Compilation function for fieldset block tag
Render a form fieldset
https://docs.djangoproject.com/en/1.8/howto/custom-template-tags/#writing-the-compilation-function
https://docs.djangoproject.com/en/1.8/howto/custom-template-tags/#parsing-until-another-block-tag
:param parser: template parser
:param token: tag name and variables
:return: HTML string
"""
try:
tag_name, fieldset_name = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError("%r tag requires a single argument" % token.contents.split()[0])
if not (fieldset_name[0] == fieldset_name[-1] and fieldset_name[0] in ('"', "'")):
raise template.TemplateSyntaxError("%r tag's argument should be in quotes" % tag_name)
nodelist = parser.parse(('endfieldset',))
parser.delete_first_token()
return FieldsetNode(nodelist, fieldset_name[1:-1])
templates/myapp/fieldset.html:
<div class="fieldset panel panel-default">
<div class="panel-heading">{{ name }}</div>
<div class="panel-body">{{ var }}</div>
</div>
templates/myapp/myform.html:
<form action="{% url 'myapp:myurl' %}" method="post">
{% csrf_token %}
{% fieldset 'General' %}
{{form.myfield1 }}
{% endfieldset %}
{# my submit button #}
</form>