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();
}
}
Related
As a learner of the django framework, I face the challenge of looping through all primary keys of a table, beginning with the first, and at the end of the sequence, to start all over again. The id field is an auto-incrementing serial field in postgres. A very basic breakdown of the concept is as follows:
models.py
...
class Event(models.Model):
id = models.BigAutoField(primary_key=True)
event_name = models.BigIntegerField(blank=True, null=False)
timestamp = models.DateTimeField(blank=True, null=True)
viewed = models.BooleanField(default=False)
views.py
def home(request, id=None):
from .models import Event
first_obj = Event.objects.order_by('id').first()
my_json = serialize('json', first_obj, fields=('event_name', 'timestamp'))
return render(request, 'home.html', {'items': my_json})
def get_next(request):
#set viewed=True on current object
.save()
return redirect('home') #move to next object
home.html
...
<form method="POST" action="{% url 'get_next' %}">
<button>confirm</button>
</form>
...
The idea is to send the my_json object to an html template. A user clicks a button, which should then mark the current object as viewed (sets viewed from False to True), then the next object should be fetched (by incremental primary key), and so on. If it has reached the last object, the first object (pk=1) should be fetched next, and the cycle continues. I would like to avoid making ajax requests and think a second url would be the most effective way of doing this, but am not sure how to proceed.
Should the current pk id be sent back and forth between requests and incremented each time? Is there a built-in method for this concept in django? What would be an efficient method of cycling through all primary keys? Any advice here on how to best structure this concept would be greatly appreciated!
When you post to get_next url, you should send the PK of the current event. Then event = get_object_or_404(Event, pk=request.post.get("pk") will give you the current event. event.viewed = True event.save().
In your home view, you should do:
event = Event.objects.filter(viewed=False).order_by("pk")
if not event.exists():
Event.objects.all().update(viewed=False) #only in Django 2.2+ I think
event = Event.objects.order_by("pk").first()
else:
event = event.first()
return ...
I think this will give you a cycle when all are viewed we set them back to not viewed and start with the first one again.
Also, if you are using integer PK's, and if you haven't defined otherwise in the Meta of your class, order_by is not necessary. By default they are ordered by PK.
I want to change my Foreign Key to Many To Many field to let the user select multiple categories in a dropdown list.
This is what I already have. After I change Foreign Key to Many To Many I'm getting milion errors, I have to get rid of on_delete=models.CASCADE which is a core of my app. What can I do? Which way should I take? Maybe add another model? I'm so confused, especially when I am a Django newbie. Thank you for your help!
MODELS
class Category(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return f'{self.name}'
class Expense(models.Model):
class Meta:
ordering = ('date', '-pk')
category = models.ForeignKey(Category, null=True,blank=True, on_delete=models.CASCADE)
name = models.CharField(max_length=50)
amount = models.DecimalField(max_digits=8,decimal_places=2)
date = models.DateField(default=datetime.date.today,db_index=True)
def __str__(self):
return f'{self.date} {self.name} {self.amount}'
The clue of the application is to let the user create a category e.g "PC". Then add some expenses like "GPU", "CPU" etc... and let the user link it to the "PC" category. And when the user wants to delete certain categories, all the expenses linked to it, gonna be deleted too. And this is the thing I have already did. BUT NOW I want to let the user search the main table of expenses by multiple categories. And here comes my problem, I don't have a clue how to do it and keep the whole application in one piece with all the functionalities.
SCREENSHOTS:
Categories View with just added PC category
Expense Add View
I don't think there is a simple answer to your question, but here are some resources that might help. First, I don't think you should change your models. From the way you described your application, I think a foreign key model with on_delete=CASCADE is good. The basic idea here is that you need to change your list view function so that it performs a query of your database. Also modify your template.html to include a search bar.
https://github.com/csev/dj4e-samples/tree/master/well
https://www.dj4e.com/lessons/dj4e_ads4
Modify Your List View To Allow The Searching
This is an example of a list view that allows you to search for a single term, and returns anything in the database that matches from any field. This isn't what you want to do exactly, but if you can get this working then you can modify the search conditions for your specific application. What is going on in the code below is that instead of return every item in my Ad table in my SQL database, I filter it based on the search. Then, I pass "ad_list" to the template view. Since I already filtered ad_list based on the search, in the template view it will only list the items that match. This is based on the DJ4E course, and you can watch the video there to get an idea of how he implements the search bar better.
from ads.models import Ad
from django.views import View
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse_lazy, reverse
from django.http import HttpResponse
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.contrib.humanize.templatetags.humanize import naturaltime
from ads.utils import dump_queries
from django.db.models import Q
class AdListView(ListView):
# By convention:
template_name = "ads/ad_list.html"
def get(self, request) :
strval = request.GET.get("search", False)
if strval :
# Simple title-only search
# objects = Ad.objects.filter(title__contains=strval).select_related().order_by('-updated_at')[:10]
# Multi-field search
query = Q(title__contains=strval)
query.add(Q(text__contains=strval), Q.OR)
objects = Ad.objects.filter(query).select_related().order_by('-updated_at')[:10]
else :
# try both versions with > 4 posts and watch the queries that happen
objects = Ad.objects.all().order_by('-updated_at')[:10]
# objects = Ad.objects.select_related().all().order_by('-updated_at')[:10]
# Augment the post_list
for obj in objects:
obj.natural_updated = naturaltime(obj.updated_at)
ctx = {'ad_list' : objects, 'search': strval}
retval = render(request, self.template_name, ctx)
dump_queries()
return retval;
Modify Your Template.html to include a search bar
<form>
<input type="text" placeholder="Search.." name="search"
{% if search %} value="{{ search }}" {% endif %}
>
<button type="submit"><i class="fa fa-search"></i></button>
<i class="fa fa-undo"></i>
</form>
PS, I think you can answer your own question better when you figure it out, so help others and post it!
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.
The Problem
We have the following setup.
Pretty standard Django class based view (inherits from CreateView, which is what I'll call it form now on).
After a successful POST and form validation, the object is created, and the user is redirect_to'd the DetailView of the created record.
Some users decide that they are not happy with the data they entered. They press the back button.
The HTML generated by the CreateView is fetched form browser cache, and repopulated with the data they entered.
To the user, this feels like an edit, so they change the data and submit again.
The result is 2 records, with minor differences.
What have we tried?
At first I thought the Post-Redirect-Get (PRG) pattern that Django uses was supposed to prevent this. After investigating, it seems that PRG is only meant to prevent the dreaded "Do you want to resubmit the form?" dialog. Dead end.
After hitting the back button, everything is fetched from cache, so we have no chance of interacting with the user from our Django code. To try and prevent local caching, we have decorated the CreateView with #never_cache. This does nothing for us, the page is still retrieved form cache.
What are we considering?
We are considering dirty JavaScript tricks that do an onLoad check of window.referrer, and a manual clean of the form and/or notice to user if the referrer looks like the DetailView mentioned earlier. Of course this feel totally wrong. Then again, so do semi-duplicate records in our DB.
However, it seems so unlikely that we are the first to be bothered by this that I wanted to ask around here on StackOverflow.
Ideally, we would tell the browser that caching the form is a big NO, and the browser would listen. Again, we already use #never_cache, but apparently this is not enough. Happens in Chrome, Safari and Firefox.
Looking forward to any insights! Thanks!
Maybe don't process the POST request when it's coming from a referrer other than the same page?
from urllib import parse
class CreateView(...):
def post(self, *args, **kwargs):
referer = 'HTTP_REFERER' in self.request.META and parse.urlparse(self.request.META['HTTP_REFERER'])
if referer and (referer.netloc != self.request.META.get('HTTP_HOST') or referer.path != self.request.META.get('PATH_INFO')):
return self.get(*args, **kwargs)
...
I know I'm late to this party but this may help anybody else looking for an answer.
Having found this while tearing my hair out over the same problem, here is my solution using human factors rather than technical ones. The user won't use the back button if after submitting from a CreateView, he ends up in an UpdateView of the newly created object that looks exactly the same apart from the title and the buttons at the bottom.
A technical solution might be to create a model field to hold a UUID and create a UUID passed into the create form as a hidden field. When submit is pressed, form_valid could check in the DB for an object with that UUID and refuse to create what would be a duplicate (unique=True would enforce that at DB level).
Here's example code (slightly redacted to remove stuff my employer might not want in public). It uses django-crispy-forms to make things pretty and easy. The Create view is entered from a button on a table of customers which passes the customer account number, not the Django id of its record.
Urls
url(r'enter/(?P<customer>[-\w]+)/$', JobEntryView.as_view(), name='job_entry'),
url(r'update1/(?P<pk>\d+)/$', JobEntryUpdateView.as_view(), name='entry_update'),
Views
class JobEntryView( LoginRequiredMixin, CreateView):
model=Job
form_class=JobEntryForm
template_name='utils/generic_crispy_form.html' # basically just {% crispy form %}
def get_form( self, form_class=None):
self.customer = get_object_or_404(
Customer, account = self.kwargs.get('customer','?') )
self.crispy_title = f"Create job for {self.customer.account} ({self.customer.fullname})"
return super().get_form( form_class)
def form_valid( self, form): # insert created_by'class
#form.instance.entered_by = self.request.user
form.instance.customer = self.customer
return super().form_valid(form)
def get_success_url( self):
return reverse( 'jobs:entry_update', kwargs={'pk':self.object.pk, } )
# redirect to this after entry ... user hopefully won't use back because it's here already
class JobEntryUpdateView( LoginRequiredMixin, CrispyCMVPlugin, UpdateView):
model=Job
form_class=JobEntryForm
template_name='utils/generic_crispy_form.html'
def get_form( self, form_class=None):
self.customer = self.object.customer
self.crispy_title = f"Update job {self.object.jobno} for {self.object.customer.account} ({self.object.customer.fullname})"
form = super().get_form( form_class)
form.helper[-1] = ButtonHolder( Submit('update', 'Update', ), Submit('done', 'Done', ), )
return form
def get_success_url( self):
print( self.request.POST )
if self.request.POST.get('done',None):
return reverse('jobs:ok')
return reverse( 'jobs:entry_update',
kwargs={'pk':self.object.pk, } ) # loop until user clicks Done
I am using django admin.
But there are currenly no delete links infront of every row. they have delete selected thing but i want to have delete and edit with every row of model.
how can i do that in django admin
class MyAdmin(models.ModelAdmin):
list_display = ('other_field', 'delete_link', 'edit_link')
def delete_link(self, obj):
info = obj._meta.app_label, obj._meta.module_name
url = reverse('admin:%s_%s_delete' % info, args=(obj.id,))
return 'Delete' % url
delete_link.allow_tags = True
delete_link.short_description = 'Delete'
def edit_link(self,obj):
return u'Edit' % (
obj._meta.app_label, obj._meta.module_name, obj.id)
edit_link.allow_tags = True
edit_link.short_description = "Edit"
UPDATE:
def action_link(self, obj):
app_name = obj._meta.app_label
url_name = obj._meta.module_name
data_id = obj.id
return """
<ul>
<li>Edit</li>
<li>Delete</li>
</ul>
""".format(
obj._meta.app_label,
obj._meta.module_name,
obj.id)
action_link.allow_tags = True
action_link.short_description = 'Actions'
Fields can be made editable inline within Django admin using the list_editable admin option: https://docs.djangoproject.com/en/1.4/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_editable
However, there's no built-in option to add a delete link to every row. That's something you would need to add yourself. You would need to use a combination of a property on the model to add to the list_display in order to get the 'Delete' text in the change list, plus some JavaScript to confirm the delete. I wouldn't fire the delete in a single action.
It's up to you whether you want to fire the delete action via Ajax, or if you want to redirect them to the delete view, etc. Hope that gets you started.