Django - access parameters of the previous request - django

The problem:
I am working on a webapp that has a 'StudentListView' that should have the following features:
Display a list of students
Have a searchbox above that allows the user to filter / search this list (Submitting 'Peter' should return all students with 'Peter' in their name)
An 'export' button should allow the user to export this (possibly filtered!) list to a .csv file
I've implemented the code below to allow the user to filter the list, which works as intended. I've also created an export_students function that creates an .csv file of all students in the supplied queryset. This function also works as intended.
However, when exporting a filtered list the program does not behave as the user expects. The user will first filter the list by providing search parameters, which will trigger an request and refresh the page. The user than presses the 'export' button, but since he did not re-submit the search parameters (why would he, the list he sees is already filtered) none are provided in the request and thus the csv file contain all students in the database, instead of the filtered selection he expects.
Possible solution
This problem would be solved if I could somehow store the search parameters of the request, and have them retrieved if the following request.GET contains the exportstudents keyword. But I am not sure how I could accomplish this. The closest I got was by using javascript to append the search params to the value of the Export button. But this meant that request.GET['exportstudents'] had the entire search query as its value (I removed most of the search/filter options in my code examples for simplicity, but these parameter strings can get really long)
I could of course parse the results with a complicated regex but seems like a very convoluted solution for a problem that probably has a much easier solution.
The code
Again, I removed most of the filter parameters for simplicity, but the code below should give an good indication of how my view functions.
The HTML appends keywords to the requests .GET parameter, which, if present, are either used to filter the queryset or trigger the export_students function.
<form method="get">
<input name="search_query" type="text" class="form-control"
value="{% if request.GET.q %}{{ request.GET.q }}{% endif %}"
placeholder="Find student"
/>
<button type="submit">Filter Students</button></span>
</form>
<form id='exportform' class="col-md-5 " method="get" action="">
<div class="input-group">
<button id='export' name="exportstudents" value="true">Export Students</button>
</div>
</form>
class StudentListView(generic.ListView)
def get_queryset(self):
field = self.request.GET.get('field', 'last_name')
qs = Student.objects.all()
search_query = self.request.GET.get('search_query', None)
if search_query:
qs = qs.filter(
Q(first_name__icontains=search_query) |
Q(last_name__icontains=search_query) |
Q(email_parents__icontains=search_query)
)
return qs
def get(self, request, *args, **kwargs):
if 'exportstudents' in self.request.GET:
qs = self.get_queryset()
file = export_students(qs)
content = 'attachment; filename="{}_{}.csv"'.format(
u'Student_export',
timezone.now().strftime('%d-%m_%H:%M')
)
response = StreamingHttpResponse(
file, content_type='text/csv')
response['Content-Disposition'] = content
return response
return super(UserListView, self).get(request, *args,
**kwargs)
The functionality that this view seeks to provide are commonplace on many websites, so obviously a solution is possible. But I am currently at a loss on how to allow users to export filtered lists when those filters have been applied to the previous queryset.

Export students is also a form. So you could add the parameter there as a hidden field - now your existing view code will just work.
<form id='exportform' class="col-md-5 " method="get" action="">
<input type="hidden" name="search_query" value="{{ request.GET.search_query }}"
<div class="input-group">
<button id='export' name="exportstudents" value="true">Export Students</button>
</div>
</form>
Alternatively, just use a single form with two buttons:
<form method="get">
<input name="search_query" type="text" class="form-control"
value="{% if request.GET.q %}{{ request.GET.q }}{% endif %}"
placeholder="Find student"
/>
<button type="submit">Filter Students</button></span>
<div class="input-group">
<button id='export' name="exportstudents" value="true">Export Students</button>
</div>
</form>
Again, this will just work (although I suspect you meant request.GET.search_query there too).

You may also be able to get access to the previous request's GET using the request.META.get('HTTP_REFERER') header on the request. But it is possible that some browsers could be configured not to send the referer.

Related

Django: Simple solution to prepend $ to form field

I would like to prepend a $ to an input field for a few of my forms. I don't want the $ sent to the backend as part of the input value. I have found a few stack overflow questions that suggest things like the updates below:
self.fields['buy_price'].localize = True
self.fields['buy_price'].widget.is_localized = True
self.fields['buy_price'].prefix = '$'
But none of these are working for me. I also would prefer to avoid adding 50+ lines of code, such as recommended in this S.O. Answer: How to represent Django money field with currency symbol in list template and plain decimal for edits?
HTML form:
{% url 'update-buy/' offer.id as buy_update_url %}
<form method="POST" action="{{ buy_update_url }}">
<div class="form-group">
<legend class="border-bottom mb-4">Update Offer to Buy a Game</legend>
{% csrf_token %}
{{ form|crispy }}
{{ form.media }}
</div>
<input type="hidden" id="hidden_desired_game_id" name="hidden_desired_game" value="{{ offer.desired_game_id }}">
<div class="form-group">
<button onclick="clearMessage()" class="post-button btn btn-outline-info" type="submit">Update</button>
</div>
</form>
Does anyone have a simple way to do this?
UPDATE:
I updated to use {% crispy form %} instead of {{ form|crispy }}, which is nicely showing the $. But now, the hidden input with id="hidden_desired_game_id" is not included in the POST request. For some reason, when rendering the form (screenshot below), that input is now BELOW the form, not inside it. Any idea how I can still have that included?
EDIT #2: I fixed the above problem by moving the input field higher up in the form. But now it looks like jquery is loading twice or something. There are 2 dropdown arrows on the right side of the Desired game field and it looks ugly. I tried using javascript to manipulate the class and played around with the css_class feature from crispy-forms, but i can't get it to only have one dropdown. Does anyone know how to fix that? Screenshot below
SOLUTION FOUND: FINAL UPDATE:
I was able to fix the above issue of 2 dropdowns by adding this javascript:
window.onload = function () {
$( ".custom-select" ).removeClass("custom-select"); // Remove dupe dropdown
}
Kinda hacky but oh well! Everything is good now
You can make use of Bootstrap Layout objects
forms.py
from crispy_forms.helper import FormHelper
from crispy_forms.bootstrap import PrependedText
class ProductForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
PrependedText('buy_price', '$')
)
instead of {{ form|crispy }} you have to use {% crispy form %}
The form field will look like

How can I delete a single file in Django?

I have a template which displays the files that a user has uploaded. I managed to make a view that allows me do delete all the files the user has uploaded, but I also would like to make it possible to delete each one individually.
I have a bootstrap card, and in the body I display each file with the delete link on the right:
<div class="card-body text-light">
{% for doc in docs %}
<ul>
<font face="Ubuntu">{{doc.document|filename}}</font>
<font face="Ubuntu" color="red">Delete</font>
</ul>
{%endfor%}
</div>
And in the card footer I use the view that deletes all files:
<div class="card-footer bg-transparent border-light">
<i class="fas fa-trash-alt" style="color:red"></i> <font face="Ubuntu" color="red"><b>Delete All Files</b></font>
</div>
My delete view is as follows:
def delete(request, **kwargs):
documents = Document.objects.filter(owner=request.user.id)
documents.delete()
cleanup_post_delete.connect(delete, request)
return redirect('main:user_panel')
The problem is, I can't figure how to delete each file individually, thought of using the objects.get() method but it really can't help me. I would need some view that targets that specific file and deletes it.
UPDATE:
So, here is how I managed the problem:
I made another view called delete_single:
def delete_single(request, id):
document = Document.objects.get(pk=id)
document.delete(id)
return redirect('main:user_panel')
But that wasn't enough, so by searching a bit I found a way around, these two classes will help in terms of security, since I found out that otherwise my file objects may be susceptible to CSRF attacks (not that would matter right now for me, since this is just a project of mine and I don't plan anything special with it, but I take it as good practice anyways):
class PermissionMixin(object):
def get_object(self, *args, **kwargs):
obj = super(PermissionMixin, self).get_object(*args, **kwargs)
if not obj.owner == self.request.user:
raise PermissionDenied()
else:
return obj
class PostDelete(PermissionMixin, DeleteView):
model = Document
success_url = reverse_lazy('main:user_panel')
Also in urls.py:
url(r'^delete_single/(?P<pk>\d+)/$', views.PostDelete.as_view(), name='delete_single')
And finally in my template:
<form action="{% url 'main:delete_single' doc.pk %}" method="post">
{% csrf_token %}
<input class="btn btn-danger" type="submit" value="Delete" />
</form>

How do you update a many to many field from a form?

I have the following two models:
class TaskFile(models.Model):
file = models.FileField(upload_to='task-files/')
def __str__(self):
return self.file.name
class Task(models.Model):
lesson = models.ManyToManyField(TaskFile, related_name='task_files')
I have a model form to update the Task object that is already created, but the many to many relationships do not show up in the form. It just shows the option to upload a file and does not show the existing files in that object.
How can I fix this?
Edit:
This is my model form code:
class TutorTaskSelectForm(forms.ModelForm):
lesson = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}))
class Meta:
model = Task
fields = ('lesson')
This is my template:
<form action="{{request.path}}" method="POST" enctype="multipart/form-data">
{%csrf_token%}
<div class="box-body">
<div class="form-group">
<label for="inputEmail3" class="col-sm-2 control-label">Number</label>
<div class="col-sm-10">
{{form.lesson}}
</div>
</div>
</div>
</form>
First, i am not a huge fan of built-in Django Forms. So i am going to suggest you a different way. A way without Django Forms.
Out of context of this question:
There are great, i mean really great, front-end libraries like
React, Vue or Angular. And they are getting more popular every
day, or even every minute. When you decide to choose one of those
fancy libraries, using Django forms doesn't make sense so much.
Anyway, If you want to keep your existing model structure, I think the best thing you can do here is updating the logic inside of your view:
def index(request):
if request.method == 'POST':
print(request.FILES.getlist)
files = request.FILES.getlist('lesson')
# #TODO: check if form is valid or files are proper etc. here
task = Task() # new task instance here
task.save()
for f in files:
task_file = TaskFile()
task_file.file = f
task_file.save() # save uploaded file to the TaskFile
task.lesson.add(task_file) # here add that file to the many to many field of Task Model
return HttpResponse('All files saved!')
else:
ctx = {
'form': TutorTaskSelectForm()
}
return render(request, 'index.html', ctx)
I tested above code. It is working. But you must clarify what you mean by saying uploading multiple files.
Do you want to select multiple files at once? Or do you want different and separate file dialog boxes for each file?
If you want to have multiple selection while picking files from browser dialog box, then above solution should work for you.
But If you want to have multiple files by picking them separately, then you need multiple inputs in your html side. Something like this:
<form action="{{ request.path }}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<input type="file" name="lesson" required />
<input type="file" name="lesson" required />
<input type="file" name="lesson" required />
<input type="submit" value="Save"/>
</form>
Note that you don't need Django forms in this case. Just create regular input files then handle them in you view. You can reach files inside a request by calling request.FILES.getlist('lesson').
But again, i wouldn't use django forms for this case. Here is the version of not using django forms:
<form action="{{request.path}}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div class="box-body">
<div class="form-group">
<label for="inputEmail3" class="col-sm-2 control-label">Number</label>
<div class="col-sm-10">
<input type="file" name="lesson" required multiple />
</div>
</div>
</div>
<input type="submit" value="Save" />
</form>
Put those lines in your html, and use the code above. This is very basic and simple. You can update it according to your requirements.

How to send dynamic hidden form variables using POST

I have a page that pulls out entries from the database as 'users' and lists them. A typical result looks like this:
John
Marty
Tom
Jane
Chris
Now I would like to click on a specific the name and go to their specific page. For this, I have a form that posts to a view that expects the user that has been "clicked"
So far, I have a form inside a loop that goes through each 'user' in 'users' table. The setup works fine but the major problem is that the form element 'name' is replaced by the last user. No matter, whose name I click it always passes the last user's username.
{% for user in users %}
<h1>{{ user.firstName }} {{ user.lastName }}</h1>
<form action="/friend_profile/" method="post" accept-charset="utf-8">
<input type="hidden" name="selectedFriend" value ={{ user.userName }}>
<button type="submit" value="view profile">
{% endfor %}
I am not using DJango forms and just using request.method == 'POST' for receiving variables.
So my dumb question would be, is there a way to dynamically create 'name' form element and submit its contents specific to the user? Right now, the form always submits the user "Chris" no matter which user I click because its the last one on the list.
Right now, the form always submits the user "Chris" no matter which user I click because its the last one on the list.
That's because you didn't close your <form> tag, so the browser sees one big bunch of nested forms.
Also, you need to quote and escape the value attribute in your hidden input:
<form action="/friend_profile/" method="post" accept-charset="utf-8">
<input type="hidden" name="selectedFriend" value="{{ user.userName|escape }}">
<button type="submit" value="view profile">
</form>

How to create a complex admin action that requires additional information?

I'm interested in creating an action for the admin interface that requires some additional information beyond the items selected. My example is bulk adding comics to a series. (Yes I know the obvious answer is to create a schema with X-to-X relationships, but bear with me for the sake of a simple example).
In this example, I've created 100 comics. After they're created, I'd like to associate them with a series object that's already been created. To execute this action within the admin, I'd like to select the items then initiate the action. I should then be asked which series object to use (via a popup, intermediate form, etc.).
I've followed the instructions here which claim to accomplish this via an intermediate form. After working with it, I'm not getting any more errors, but the action itself isn't being executed either - the forloop never gets executed. Instead, it returns to the admin list of comics with the message: "No action selected."
my admin.py method:
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.http import HttpResponseRedirect
def addSeries(self, request, queryset):
form = None
if 'cancel' in request.POST:
self.message_user(request, 'Canceled series linking.')
return
elif 'link_series' in request.POST:
form = self.SeriesForm(request.POST)
if form.is_valid():
series = form.cleaned_data['series']
for x in queryset:
y = Link(series = series, comic = x)
y.save()
self.message_user(request, self.categorySuccess.render(Context({'count':queryset.count(), 'series':series})))
return HttpResponseRedirect(request.get_full_path())
if not form:
form = self.SeriesForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})
return render_to_response('setSeries.html', {'comics': queryset, 'form': form, 'path':request.get_full_path()}, context_instance=RequestContext(request))
addSeries.short_description = 'Set Series'
My intermediate form setSeries.html:
<!DOCTYPE html>
<html>
<head>
<title>Create Series Links</title>
</head>
<body>
<h1>Create Series Links</h1>
<p>Choose the series for the selected comic(s):</p>
<form method="post" action="{{ path }}">
<table>
{{ form }}
</table>
<p>
<input type="hidden" name="action" value="changeSeries" />
<input type="submit" name="cancel" value="Cancel" />
<input type="submit" name="link_series" value="link_series" />
</p>
</form>
<h2>This categorization will affect the following:</h2>
<ul>
{% for comic in comics %}
<li>{{ comic.title }}</li>
{% endfor %}
</ul>
</body>
</html>
One thing I notice is that your action’s method is “addSeries”, but in the form you’re calling it “changeSeries”.
In your ModelAdmin, you should have a line like this:
actions = ['addSeries']
If that’s the line you have, then you need to change:
<input type="hidden" name="action" value="changeSeries" />
to:
<input type="hidden" name="action" value="addSeries" />
That’s how Django’s admin knows which action was selected. When you have an intermediary form between choosing the action and performing the action, you’ll need to preserve the action name from the select menu on the admin interface.