Custom form fields in Django - python

I know how to create forms in forms.py and views.py using models.py, but to want to create a birthdate field. As everyone knows we can do this birthdate = models.DateTimeField(). However, the issue is the user would have to input their birthdate by typing YYYY-MM-DD-HH-MM-SS.
So does anyone here have an elegant way for users to input their birthday?
Thanks

I believe you are looking for a nicer DateTimeWidget, not the field itself. Secondly, unless it's really necesary, you would more likely use a DateField rather than DateTimeField to record user's birthdate ;)
here's just a one example of custom datetime input widget, but there are more:
https://github.com/asaglimbeni/django-datetime-widget
in the forms, you would do:
birthdate = forms.DateTimeField(widget= ...)
cheers

Of course. Tie a date-picker to the birthday input.
See jQuery UI's datepicker widget.
The approach is:
User picks a date from datepicker widget.
The brithday input get's automatically populated as per the selected date.
After form submission, the value in the input goes to the backend.
UPDATE:
Example:
In your template:
<form action=".">
...
{{ form.birthdate }}
...
</form>
<script>
$(function() {
$( "#id_birthdate" ).datepicker({
changeYear: true,
changeMonth: true,
});
});
</script>
The id of the birthdate input is definitely going to be id_birthdate, because that is how Django assigns ids to form fields, so that is what you need to pass in the script's function.
Also, include jqueryui.js in your project.

You could use models.DateField and custom DATE_INPUT_FORMATS if needed:
https://docs.djangoproject.com/en/dev/ref/settings/#date-input-formats

Related

Django crispy forms inline label and input field

I have a Django ModelForm containing 3 fields: name, lastname, email. What I want is to adjust the crispy form to have each field along with it's label in one line, so the form will look like this:
Name: <input name>
Lastname: <input lastname>
Email: <input email>
<button Submit>
I tried with FormHelper and Layout and bootstrap inline attribute but all I achieved was having all elements in the form in one line.
I'm really bad at frontend and I'm stuck.
Assuming your settings.py contains:
CRISPY_TEMPLATE_PACK = 'bootstrap4'
you probably want to override the template bootstrap4/field.html which is the one that places the label and the various possible input field together.
to do so, you can copy the templates/bootstrap4/field.html file from the crispy_form package into your templates/bootstrap4/field.html in your application and have it modified as per your needs. When the template engine will render the form fields, it will find your modified field.html before the one in the original template.
you may also want to be refered to the template pack uni_form which, without css, renders the fields almost the way you want. You can use it as is or get yourself inspired by the way it is structured to modify your own field.html.
I managed to achieve my goal by simply changing {{ my_form|crispy }} to {{ my_form.as_p }} in my html code.

Create a confirmation message from model in Django

I have an app in Django that has multiple models. I have a particular model like this:
models.py
class MyModel(models.Model):
model_id= models.AutoField(primary_key=True)
model_date = models.DateTimeField(verbose_name="Label 1")
model_counter = models.IntegerField(blank=True, null=True)
admin.py
class MyModelAdmin(admin.ModelAdmin):
list_display = ('model_id', 'model_date ', 'model_counter ')
list_filter = (
('model_date', DropdownFilter)
)
def has_delete_permission(self, request, obj=None):
return False
def get_actions(self, request):
actions = super().get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
readonly_fields = ['model_counter ']
admin.site.register(MyModel, MyModelAdmin)
What I need is that the user confirm that he wants to save the model, even if the date is greater than today, in doing so the user can create a model that can be wrong for other users, but sometime it's correct that the date is greater than today.
I cannot use a custom HTML form, only the default one.
The user must interact someway with the page, and give direct aknowledgement that he knows that is saving a new model that can be dangerous for other users. The user must be able to abort the saving or modify the date.
So I tried to use another field of the model to store the counter:
def clean(self):
condition = False
if self.model_counter is None:
condition = True
else:
condition = self.model_counter == 1
if condition :
self.model_counter = 1
raise ValidationError("Attention, the date inserted is after the current date, click the SAVE button another time to proceed with the saving")
As a counter I use another field of the same model. I'm not able to make the updating of the counter working. From what I have understood, the lifecycle of validation prevent me to alter in the proper way the state of the istance of the model that the code it's saving, so the updating of the field model it's ignored.
There are is any way to achieve my goal? I used the model field for storing the value of counter because I'm not able in another way. I don't care where is the counter. I don't care also to use the message system of Django or a popup. I need only to force the user under some condition to make an interaction that force him to continue or abort the saving.
Edit
I added also the code in the admin.py for more clearness. I modified only the models and the admin, invoke the command: python3 manage.py inspectdb > models.py and I got all the code generated. That it's the standard procedure for this things in my company. So I cannot add (or I don't how) code to the Web pages generated from Django.
I think you would be best to use some JavaScript here. Where you add a click event to the submit button where a modal/dialag asks the user to confirm. If they say Yes, then you can submit the form.
For example with dialog (you can make it modal if you want):
HTML
<form id="myModelForm" method="POST">
{% csrf_token %}
{{ form.as_p }}
<button id="myModelSave">{% trans "Save" %}</button>
</form>
JS
let saveButton = document.getElementById('myModelSave');
saveButton.onclick = function() {
let accepted = confirm("Are you sure you want to save?");
if (accepted) {
document.getElementById('myModelForm').submit();
}
}

Django-taggit tag value retrieval and formatting failing

I am trying to implement a tagging process for profiles so you can add your hobbies for example.
I have chosen django-taggit as it seemed quite simple and does what I need it to, plus don't really know how to do it myself from scratch.
I have managed to make it work to some extent but I am having issues with 3 things:
Not really sure what's the best way to control the form field for these tags as I generate the form automatically with widget adjustments in meta function of the form, but it might work fine after resolving the below two issues.
When there is no data for the field hobbies (tags) the field gets populated with a single tag of value "[]" as per below image.
When I add a tag of "music" and submit the form after I reload the page I get this "[]" as per image. I assumed this will be dealt with by the library, but I cannot see another similar scenario online.
When I try adding another tag of "games" and save and reload, the below happens. The initial value gets wrapped again.
My model is:
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
hobbies = TaggableManager()
My form is:
class UserProfileForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ['hobbies',]
def __init__(self, *args, **kwargs):
super(UserProfileForm, self).__init__(*args,**kwargs)
self.fields['hobbies'].widget = forms.TextInput()
self.fields['hobbies'].widget.attrs['data-role'] = "tagsinput"
self.fields['hobbies'].widget.attrs['class'] = "form-control"
self.fields['hobbies'].required = False
My view function is:
if request.method == 'POST':
user_profile = UserProfile.objects.get(user=request.user)
form = UserProfileForm(request.POST, instance=user_profile)
print(form)
if form.is_valid():
obj = form.save(commit=False)
obj.user = request.user
obj.save()
print("Form valid")
form.save_m2m()
Using:
<script src="/static/js/tagsinput.js"></script>
<link rel="stylesheet" href="{% static 'css/tagsinput.css' %}" />
I had this exact same problem.
One solution is to apply the data-role="tagsinput" AFTER you turn a list of tags into a comma-separated string for the form.
Here is that solution:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, **kwargs):
self.fields['tags'].widget.attrs['value'] = ", ".join(list(self.instance.tags.names()))
self.fields['tags'].widget.attrs['data-role'] = "tagsinput"
Output:
As you can see, there's a problem with quotes appearing around tags that are multi-word. It also causes new tags with quotes to be saved to the database.
If double-quotes didn't appear around multi-word phrases, this would be the most elegant solution. If someone solves this in the future, drop a note!
My template is this:
<div class="m-3 p-3 border">
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">Save Form</button>
</form>
</div>
I know I can use a template tag to strip the extra quotes from the tag field itself, but then I'd have to go through and create all the form fields manually just to set the tags template tag.
For the time being, my solution is to simply use Javascript and just modify the Meta widgets section of the form.
FINAL ANSWER (for now):
forms.py
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
widgets = {
'tags': forms.TextInput(attrs={
"data-role": "tagsinput",
})
}
custom.js - put this script on the page that loads the form.
document.addEventListener("DOMContentLoaded", function(event) {
let tags_input = document.querySelector('#id_tags');
let tags_input_value = tags_input.value;
let new_value = [...tags_input_value.matchAll(/<Tag:\s*([\w\s]+)>/g)].map(([, m]) => m).join(', ')
tags_input.setAttribute('value', new_value);
}
So all we're doing is modifying the front-end presentation, and leaving all the backend internal forms functionality untouched.
So after quite a few (hundreds) of tests, I finally narrowed down where the issue was and tried to go around it with successful result.
It seems the data got amended into tag objects through tagsinput library I was using. Only when the "data-role" was specified as "tagsinput" in the forms.py the data would already come to html side as those objects and be shown incorrectly. So instead I wanted to keep the data clean and only apply data-role='tagsinput' in the end for visual aspect, which I did using:
var hobbiesTags = document.getElementById("id_hobbies");
if(hobbiesTags){
var att = document.createAttribute("data-role");
att.value = "tagsinput";
hobbiesTags.setAttributeNode(att);
};
And that resulted in the below. Maybe there are better ways to do this, I'm not sure, but it's a pretty clean solution. Share your alternatives.

Readonly form field in edit view - Flask-Admin

I tried to set a form field as readonly just for the edit view in a Flask-Admin application. Following the answers to this question I know that if I have this
class MyView(ModelView):
form_widget_args = {
'title': {
'readonly': True
}
}
I can set a form field as readonly, but this is applied to both create and edit views. How can I apply an argument to the edit view only?
A pure python solution could be using the on_form_prefill callback function that admin provides, and it's only run in the edit view. You wouldn't need form_widget_args in that case.
You could have something like this to edit the form dynamically, making that field read only:
class MyView(ModelView):
def on_form_prefill(self, form, id):
form.title.render_kw = {'readonly': True}
I would do a little workaround for this. Create a custom edit template and add it to your class. The standard edit template you find in the flask repository on github (standard edit template)
class MyView(ModelView):
edit_template = 'my_custom_edit_template.html'
And in your custom edit template make a javascript function which disables your element. So it's only disabled in edit view and not in the create view.
{% block tail %}
{{ super() }}
{{ lib.form_js() }}
<script>
window.onload = function () {
document.getElementById("myfield_id").disabled = true;
}
</script>
{% endblock %}
It's maybe not the best solution but it works for me. I think it should also be possible to change the jinja2 template to directly disable the filed. I tried: {{ form.myfield_id(disabled=True) }} but then it renders my field twice... but my first approach works.
Another way to do it is to use form_edit_rules and form_create_rules. Display the fields you only want in create view, and add the field you want to be displayed in edit view and make it read-only
form_create_rules = (
'col1', 'col2', 'col3')
form_edit_rules = (
'col1', 'col2', 'col3', 'col4')
form_widget_args = {
'col4': {
'disabled': True
}
}
I think you can do it by checking the value you are trying to edit in the method that handles edit.
For eg:
You are trying to edit a value with id 1, then in the views py file you can check for the that value and add the code.
def editdata(id=None):
if id:
form.title.render_kw = {'readonly': True}
#rest of the code

Django autocomplete_light and cities_light - invalid choice

I have a model with a location field that is mapped to cities_light.city and I'm using an autocomplete field that allows users to type in their city and have it autocompleted to the correct/valid location model instance.
class Profile(models.Model):
location = models.ForeignKey(City, blank=True, null=True)
class ProfileForm(ModelForm):
class Meta:
model = Profile
fields = ('location')
widgets = {
'location': autocomplete_light.TextWidget(CityAutocomplete, autocomplete_js_attributes={'placeholder':'City, Country', 'minimum_characters':4})
}
The form field works exactly as advertised and a list of autocomplete options are shown. However when I save the form/model I get a validation error which seems to be caused by the field not being translated into the primary key of the City model instance.
Select a valid choice. That choice is not one of the available choices.
I'm guessing I need to extend the AutocompleteModelBase like the CityAutocomplete implemented below but I'm not sure how and I've been unable to find a working example.
class CityAutocomplete(autocomplete_light.AutocompleteModelBase):
search_fields = ('search_names',)
https://github.com/yourlabs/django-cities-light/blob/master/cities_light/contrib/autocompletes.py
Thank for any assistance and I'm sorry if my question is poorly formatted.
Your problem is not specific to django-autocomplete-light. What you're doing has no chance to work and here's why:
the form field for a ForeignKey like location is a ModelChoiceField by default,
ModelChoiceField accepts values which are pks of models in ModelChoiceField.queryset, which is TheModel.objects.all() by default,
the TextWidget widget is a TextInput,
a TextInput widget is just an <input type="text" />,
the value of a <input type="text" /> is directly sent to the server on form submission.
As a result, selecting a couple of cities like "Lisboa" and "Madrid" with a text widget will look like::
<input type="text" value="Lisboa, Madrid" name="location" />
Which means that the form will post {'location': 'Lisboa, Madrid'}. While this is good for a CharField, it won't work for a ModelMultipleChoiceField which would expect something like {'location': [3,5]} where 3 would be the pk of Lisboa and 5 the pk of Madrid.
In the same fashion, a ModelChoiceField would expect {'location': 3} which autocomplete_light.ChoiceWidget is able to do.
To fix this, use a ChoiceWidget instead of a TextWidget. I have clarified this in the tutorial I hope it is better now.

Categories