Django - add to favourites button in ListView - python

I want to create add to favourites feature in ListView but I am struggling with passing product slug in each item.
I have a table with the product name and Action (Add/Remove buttons). I want to allow users to add a specific product to favourites or remove it from there.
models.py
class Product(models.Model):
[...]
favourite = models.ManyToManyField(User, default=None, blank=True, related_name='favourite_product')
slug = models.SlugField(unique=True, blank=True, max_length=254)
views.py
#login_required
def products_in_favourites(request, slug):
product = get_object_or_404(Product, slug=slug)
added_to_favourite = False
if product.favourite.filter(id=request.user.id).exists():
product.favourite.remove(request.user)
added_to_favourite = False
else:
product.favourite.add(request.user)
added_to_favourite = True
context = {
'object': product,
'added_to_favourite': added_to_favourite,
}
if request.is_ajax():
html = render_to_string('favourite.html', context, request=request)
return JsonResponse({'form': html})
class AddToFavourite(LoginRequiredMixin, ListView):
template_name = "AddToFavourite.html"
model = Product
queryset = Product.objects.all()
context_object_name = 'product_list'
def get_context_data(self, **kwargs):
context = super(AddToFavourite, self).get_context_data(**kwargs)
product_item = get_object_or_404(Product, slug=self.kwargs['slug']) # how to pass slug of each product item here?
added_to_favourite = False
if product_item.cart.filter(id=self.request.user.id).exists():
added_to_favourite = True
context['added_to_favourite'] = added_to_favourite
AddToFavourite.html
{% block body %}
<section class="container">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Product</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody id="favourite">
{% for product in product_list %}
<tr>
<td>{{product.title}}</td>
<td>
{% include 'favourite.html' %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
{% endblock body %}
{% block javascript_files %}
<script>
$(document).ready(function(event){
$(document).on('click', '#favourite', function(event){
event.preventDefault();
var slug = $(this).attr('value');
$.ajax({
type: 'POST',
url: '{% url "products_in_favourites" product.slug %}',
data: {
'slug':slug,
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
dataType: 'json',
success: function(response){
$('#favourite-section').html(response['form'])
},
error: function(rs, e){
console.log(rs.responseText);
},
});
});
});
</script>
{% endblock javascript_files %}
favourite.html
<form action="{% url 'products_in_favourites' product.slug %}" method="POST" class="mx-3">
{% csrf_token %}
{% if added_to_favourite %}
<button type="submit" id="favourite" name="product_slug", value="{{product.slug}}">Remove</button>
{% else %}
<button type="submit" id="favourite" name="product_slug", value="{{product.slug}}">Add</button>
{% endif %}
</form>
urls.py
urlpatterns = [
path('add-to-favourite/', views.AddToFavourite.as_view(), name='cost_calculator'),
path('add-to-favourite/<slug:slug>', views.products_in_favourites, name='products_in_favourites'),
]

You are sending your slug not in a ListView you are sending it
<form action="{% url 'products_in_favourites' product.slug %}" method="POST" class="mx-3">
to products_in_favourites function, and doing it wrong at recieving part,
You are not sending it to a function as slug, you are sending it as data with post reqeust, and so you should recieve it same way:
#login_required
def products_in_favourites(request):
#print all data to see what is coming to this view
print(request.POST)
slug = request.POST['product_slug']
product = get_object_or_404(Product, slug=slug)
Also it is better to pass pk (primary key) instead of slug, primary key has indexes by default in database, so your query will be much much faster if you use pk.

Related

Like button is working but not changing to Unlike in django

I've been trying to debug this issue for a day together with my expert coder but we were unable to identify the issue here. The like button works perfectly all right and I am able to query the total likes, but the Clap button does not change to unlike and we are unsure why. I believe the error lies in either the html line:
{% if liked and post.slug == blog_post.slug %} Unlike {% else %} Like {% endif %}
or the django line:
context["liked"] = True if blog_post.likes.filter(id=request.user.id).exists() else False
Big thanks if you can identify the issue here!
views.py
def home_feed_view(request, *args, **kwargs):
context = {}
blog_posts = BlogPost.objects.all()
context['blog_posts'] = blog_posts
if request.method=="POST":
slug=request.POST["submit_slug"]
blog_post = get_object_or_404(BlogPost, slug=slug)
context['blog_post'] = blog_post
context["liked"] = True if blog_post.likes.filter(id=request.user.id).exists() else False
context['total_likes'] = blog_post.total_likes()
type_of_post = TypeofPostFilter(request.GET, queryset=BlogPost.objects.all())
context['type_of_post'] = type_of_post
paginated_type_of_post = Paginator(type_of_post.qs, 13 )
page = request.GET.get('page')
post_page_obj = paginated_type_of_post.get_page(page)
context['post_page_obj'] = post_page_obj
return render(request, "HomeFeed/snippets/home.html", context)
def LikeView(request, slug):
context = {}
user = request.user
if not user.is_authenticated:
return redirect('must_authenticate')
post = get_object_or_404(BlogPost, slug=slug)
liked = False
if post.likes.filter(id=request.user.id).exists():
post.likes.remove(request.user)
liked = False
else:
post.likes.add(request.user)
liked = True
return redirect(request.META.get('HTTP_REFERER', ''))
<td class="table-primary">
<form action="{% url 'HomeFeed:like_post' post.slug %}" method="POST">
{% csrf_token %}
<button type="submit" name="submit_slug" value="{{ post.slug }}" class='btn btn-primary btn-sm'>
{% if liked and post.slug == blog_post.slug %} Unlike {% else %} Like
{% endif %}
</button>
{{ post.total_likes }} Clap {{ post.total_likes|pluralize }}
</form>
</td>
urls.py
path('<slug>/like/', LikeView, name='like_post'),
models.py
class BlogPost(models.Model):
chief_title = models.CharField(max_length=50, null=False, blank=False)
body = models.TextField(max_length=5000, null=False, blank=False)
likes = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='blog_posts', blank=True)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
slug = models.SlugField(blank=True, unique=True)
Ok so here's a much simpler way to perform a check if a model exist in a m2m field in another model:
html
{% if request.user in blog_post.likes.all %} Unlike {% else %} Like {% endif %}
view
if request.user in blog_post.likes.all():
P.S. In your model you should rename the field likes to likers since its a relation with a User model not a Like model :)
EDIT
So, to easily display a list of posts and their like buttons accordingly on a single page you wanna do this in your template:
views.py
def posts(request):
blog_posts = BlogPost.objects.all()
return render(request, 'index.html', {'blog_posts': blog_posts})
index.html
{% for post in blog_posts %}
<h1>{{ post.chief_title }}</h1>
<p>{{ post.author }} says: </p>
<b>{{ post.body }}</b>
<p> This post is liked by: </p>
{% for user in post.likes %}
<p>{{ user.username }}</p>
{% endfor %}
{% if request.user not in post.likes.all %}
Like
{% else %}
Unlike
{% endif %}
{% endfor %}

Django Image not uploading

I am unable to have the file in a inlineformset_factory actually upload to the DB or static folder. The form completes then executes succeful_url. Im just not understanding why this isn't uploading the files after submit. Any help is greatly appreciated.
Edit: I am not getting any errors at all.
[12/Mar/2020 15:04:02] "POST /agent/listings/new/ HTTP/1.1" 302 0
[12/Mar/2020 15:04:02] "GET /agent/dashboard/ HTTP/1.1" 200 6915
Model.py
class Listing(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4,editable=False)
agent = models.ForeignKey(CustomUser, on_delete=models.DO_NOTHING)
class Images(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
image = models.ImageField(upload_to="listing/images/")
listing_id = models.ForeignKey(Listing, on_delete=models.DO_NOTHING)
Forms.py
class ListingImage(forms.ModelForm):
class Meta:
model = Images
exclude = ()
ListingImageFormSet = inlineformset_factory(Listing, Images, fields=['image'], form=ListingImage, extra=2)
Views.py
class AgentNewListing(CreateView):
model = Listing
fields = ['agent']
template_name = 'agent/new_listing.html'
success_url = '/agent/dashboard/'
def get_context_data(self, **kwargs):
data = super(AgentNewListing, self).get_context_data(**kwargs)
if self.request.POST:
data['listing_form'] = ListingImageFormSet(self.request.POST)
else:
data['listing_form'] = ListingImageFormSet()
return data
def form_valid(self, form):
context = self.get_context_data()
listingform = context['listing_form']
with transaction.atomic():
form.instance.agent = self.request.user
self.object = form.save(commit=False)
if listingform.is_valid():
listingform.instance = self.object
listingform.save()
return super(AgentNewListing, self).form_valid(form)
def dispatch(self, request, *args, **kwargs):
if not is_valid_agent(self.request.user):
return redirect('/agent/signin/')
return super(AgentNewListing, self).dispatch(request, *args, **kwargs)
Template.htlm
{% extends 'agent/agent_base.html' %}
{% block agent_body %}
{% load crispy_forms_tags %}
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">New Listing</h1>
</div>
<div class="col-sm-6 col-lg-6 ml-2">
<form action="" method="post" enctype="multipart/form-data">{% csrf_token %}
{{ form|crispy }}
<table class="table">
{{ listing_form.management_form|crispy }}
{% for form in listing_form.forms %}
{% if forloop.first %}
<thead>
<tr>
{% for field in form.visible_fields %}
<th>{{ field.label|capfirst }}</th>
{% endfor %}
</tr>
</thead>
{% endif %}
<tr class="">
{% for field in form.visible_fields %}
<td>
{# Include the hidden fields in the form #}
{% if forloop.first %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% endif %}
{{ field.errors.as_ul }}
{{ field }}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
<input class="btn btn-primary" type="submit" value="Submit"/> back to the list
</form>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.formset/1.2.2/jquery.formset.js"></script>
<script type="text/javascript">
$('.formset_row').formset({
addText: 'add family member',
deleteText: 'remove',
prefix: 'familymember_set'
});
</script>
{% endblock %}
Settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
Urls.py
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
The problem was I wasn't calling the self.request.FILES in the inlineformeset. The solution is in the view and looks like this.
def get_context_data(self, **kwargs):
data = super(AgentNewListing, self).get_context_data(**kwargs)
if self.request.POST:
data['listing_form'] = ListingImageFormSet(self.request.POST, self.request.FILES)
else:
data['listing_form'] = ListingImageFormSet()
return data

request.session isn't saved or is somehow modified

It seems that my request.session isn't saved or is somehow modified in other ways, despite using request.session.modified = True after changing the value.
Here is the debug output:
This should run first.
This should run second.
I have set main category to 2 (Shoes)
Main category is 1 (Books)
The code below should display a different ModelForm based on selected category, however when I select, for instance, Books, ShoesModelForm is displayed and vice-versa. This currently works as follows: whenever a value in a combobox is changed, two AJAX requests are fired. First one calls a view (load_categories) that renders another if a category has children, otherwise returns an HttpResponse which stops attaching the listener. Second one calls a view (load_modelform) that renders a specific ModelForm if selected category is root node (main category).
mappings = {
'1': BookProductForm,
'2': ShoesProductForm
}
def load_categories(request):
print("This should run first.")
category_id = request.GET.get('category')
request.session['category'] = category_id
request.session.modified = True
if Category.objects.get(id=category_id).is_root_node():
request.session['main_category'] = category_id
request.session.modified = True
print(f"I have set main category to {request.session['main_category']} ({Category.objects.get(id=category_id).name})")
subcategories = Category.objects.get(id=category_id).get_children()
if subcategories:
return render(request, 'products/category_dropdown_list_options.html', {'subcategories': subcategories})
return HttpResponse('leaf_node')
def load_modelform(request):
print("This should run second.")
main_category = request.session['main_category']
print(f"Main category is {main_category} ({Category.objects.get(id=main_category).name})")
form = mappings[main_category]
return render(request, 'products/category_modelform.html', {'form': form})
#login_required
def product_create_view(request):
if request.method == 'POST':
category = request.session.get('main_category')
create_product_form = mappings[category](request.POST)
if create_product_form.is_valid():
create_product_form.save()
return render(request, 'products/product_create.html', {
'categories': Category.objects.filter(parent=None)
})
What might be going on here?
#EDIT:
I'm including my template with jQuery code that contains Ajax requests:
{% extends 'pages/base.html' %}
{% load static %}
{% block cssfiles %}
<link rel="stylesheet" type="text/css" href="{% static 'products/css/create.css' %}">
{% endblock %}
{% block content %}
<h1>Create a product</h1>
<div class='modelform'>
<form method='POST' id='productForm' data-products-url="{% url 'products:ajax_load_categories' %}" data-modelform-url="{% url 'products:ajax_load_modelform' %}">
{% csrf_token %}
<div class='categories'>
<label>Category</label>
<select>
{% for category in categories %}
<option value="{{ category.pk }}">{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class='the-rest'>
{{ form.non_field_errors }}
{% for field in form %}
{% ifnotequal field.name 'category' %}
{{ field.label_tag }} {{ field }}
{{ field.errors }}
{% endifnotequal %}
{% endfor %}
</div>
<input type="submit" name="" value="Submit">
</form>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
var $r_ = function() {
var url = $("#productForm").attr("data-products-url");
var categoryId = $(this).val();
var toRemove = $(this).nextAll('select');
$.ajax({
url: url,
data: {
'category': categoryId
},
success: function (data) {
if (data != 'leaf_node') {
toRemove.remove();
$(".categories").append(data);
}
else {
toRemove.remove();
}
}
});
var url2 = $('#productForm').attr('data-modelform-url');
setTimeout(function() {
$.ajax({
url: url2,
data: {
'category': categoryId
},
success: function (data) {
if (data != 'dont_change_modelform') {
$('.the-rest').empty();
$('.the-rest').append(data);
}
}
});
}, 100);
}
$(document).on('change', 'select', $r_);
</script>
{% endblock %}

Django CSRF_TOKEN issue with Edge only

I'm trying my django application through different browsers (Chrome, Firefox, IE11 and Edge) and I got an issue with the csrf_token and Edge only.
This issue is in reference with my django form.
My view file :
class ManageDocView(AdminRequiredMixin, View):
""" Render the Admin Manage documents to update year in the filename"""
template_name = 'omcl/manage_doc_form.html'
form_class = ManageDocForm
success_url = 'omcl/manage_doc_form.html'
#staticmethod
def get_title():
return 'Change Document Title'
def get(self, request):
form = self.form_class()
context = {
"form": form,
"title": self.get_title()
}
return render(request, self.template_name, context)
def post(self, request):
form = self.form_class()
query_document_updated = None
query_omcl = None
query_document = None
if "submitButton" in request.POST:
omcl_list = request.POST.get('omcl_list', False)
query_omcl = Omcl.objects.get(id=omcl_list)
query_document = Document.objects.filter(omcl=omcl_list)
form.fields['omcl_list'].initial = query_omcl
elif "UpdateDocument" in request.POST:
checkbox_id = request.POST['DocumentChoice']
checkbox_id_minus_1 = int(checkbox_id) - 1
query_document_updated = Document.objects.get(id=checkbox_id)
omclcode = query_document_updated.omcl.code
src_filename = query_document_updated.src_filename
filename, file_extension = os.path.splitext(src_filename)
category = query_document_updated.category
if category == "ANNUAL":
category = "ANNUAL_REPORT"
year = self.request.POST['pts_years']
# Create the new document title updated by the new year
new_document_title = f"{year}_{category}_{omclcode}_{checkbox_id_minus_1} - {src_filename}"
# Create the new document file updated by the new year
new_document_file = f"omcl_docs/{omclcode}/{year}_{category}_{omclcode}_{checkbox_id_minus_1}{file_extension}"
# Get file.name in order to rename document file in /media/
document_path = query_document_updated.file.name
try:
actual_document_path = os.path.join(settings.MEDIA_ROOT, document_path)
new_document_path_temp = f"{settings.MEDIA_ROOT}/{new_document_file}"
new_document_path = os.rename(actual_document_path, new_document_path_temp)
except FileNotFoundError:
messages.error(self.request, _(f"Document {src_filename} doesn't exist on the server"))
return redirect('manage_doc')
else:
# Assign modifications to selected document and save it into the database
query_document_updated.title = new_document_title
query_document_updated.file = new_document_file
query_document_updated.save()
messages.success(self.request, _(f"The modification has been taken into account"))
context = {
'form': form,
'query_omcl': query_omcl,
'query_document': query_document,
'query_document_updated': query_document_updated,
'title': self.get_title(),
}
return render(request, self.template_name, context)
My forms file :
class ManageDocForm(forms.Form):
def __init__(self, *args, **kwargs):
super(ManageDocForm, self).__init__(*args, **kwargs)
omcl_list = forms.ModelChoiceField(
queryset=Omcl.objects.filter(is_obsolete=False),
label=_('OMCL Choice'),
widget=ModelSelect2Widget(
model=Omcl,
search_fields=['code__icontains', 'name__icontains'],
attrs={'data-placeholder': "Please select an OMCL"}
)
)
now = datetime.today().year
year_choices = ((i, str(i)) for i in range(now, now - 30, -1))
pts_years = forms.ChoiceField(
label='PTS years',
choices=year_choices,
required=True,
widget=Select2Widget(
attrs={'data-placeholder': "Please select a new year"}),
)
My template file with a little part of javascript :
{% block extra_script %}
<!-- Submit OMCL list with change and not submit button + Previous/Next pagination button -->
<script>
$('#select-omcl-form').on('change', function () {
$(this).submit();
});
</script>
{% endblock %}
{% block main %}
<h2>{{ title }}</h2>
<div class="row manage-doc">
<div class="col-md-12">
<form id="select-omcl-form" name="select-omcl-form" action="" method="POST">
{% csrf_token %}
<fieldset>
<legend><span class="name">{% trans 'Select an OMCL' %}</span></legend>
{{ form.omcl_list }}
<input type="hidden" name="submitButton">
</fieldset>
</form>
</div>
</div>
<br/>
<div class="row manage-doc">
<div class="col-md-12">
<fieldset>
<legend><span class="name">{% trans 'Select a document' %}</span></legend>
<form action="" method="POST">
{% csrf_token %}
<div id="table-document">
<table id="document-table" class="table table-bordered table-striped table-condensed table_model">
<thead>
<tr>
<th id="radio-column"></th>
<th id="document-title-column">{% trans 'Document title' %}</th>
</tr>
</thead>
<tbody>
{% for document in query_document|dictsortreversed:'title' %}
<tr>
<td><input type="radio" class="radio-document" id="document-radiobox" name="DocumentChoice"
value="{{ document.id }}"></td>
<td>{{ document.title }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<br><br>
<legend><span class="name">{% trans 'Select a new year' %}</span></legend>
{{ form.pts_years }}
<button class="btn btn-default" id="document-button" type="submit"
name="UpdateDocument">{% trans "Change year" %}</button>
</form>
</fieldset>
<br>
</div>
</div>
{% endblock main %}
A gif presentation :
This is a little gif which explains the process and the issue according to csrf_token only with Edge browser :
Link to my gif
What I tried :
I tried to add CSRF_COOKIE_DOMAIN in my settings.py file but it doesn't work.
Do you have any idea ? It's pretty weird because I don't have any issue with others browsers. Ony with Microsoft Edge.
Cookies are allowed in my browser.
I found the issue thanks to my collegue. I used redirect but I had to use render because if I redirect, the CSRF_TOKEN is not actualized and it sends a second POST request with the previous token.
So it should be :
except FileNotFoundError:
messages.error(self.request, _(f"Document {src_filename} doesn't exist on the server"))
context = {
'form': form,
'query_omcl': query_omcl,
'query_document': query_document,
'query_document_updated': query_document_updated,
'title': self.get_title(),
}
return render(request, self.template_name, context)
Instead of :
except FileNotFoundError:
messages.error(self.request, _(f"Document {src_filename} doesn't exist on the server"))
return redirect('manage_doc')

Multiple Forms and Formsets in CreateView

I have 2 models, Father and Son.
I have a page to register Father. On the same page I have a formset to register Son.
On page has a button "more" to add another Father and their respective Son on the same page.
Does anyone have any examples using CreateView?
Class based views are still new, so I'll write this out. The process is simple:
First, create the forms for your objects. One of the forms will be repeated. Nothing special to be done here.
class SonInline(ModelForm):
model = Son
class FatherForm(ModelForm):
model = Father
Then, create your formset:
FatherInlineFormSet = inlineformset_factory(Father,
Son,
form=SonInline,
extra=1,
can_delete=False,
can_order=False
)
Now, to integrate it with your CreateView:
class CreateFatherView(CreateView):
template_name = 'father_create.html'
model = Father
form_class = FatherForm # the parent object's form
# On successful form submission
def get_success_url(self):
return reverse('father-created')
# Validate forms
def form_valid(self, form):
ctx = self.get_context_data()
inlines = ctx['inlines']
if inlines.is_valid() and form.is_valid():
self.object = form.save() # saves Father and Children
return redirect(self.get_success_url())
else:
return self.render_to_response(self.get_context_data(form=form))
def form_invalid(self, form):
return self.render_to_response(self.get_context_data(form=form))
# We populate the context with the forms. Here I'm sending
# the inline forms in `inlines`
def get_context_data(self, **kwargs):
ctx = super(CreateFatherView, self).get_context_data(**kwargs)
if self.request.POST:
ctx['form'] = FatherForm(self.request.POST)
ctx['inlines'] = FatherInlineFormSet(self.request.POST)
else:
ctx['form'] = Father()
ctx['inlines'] = FatherInlineFormSet()
return ctx
Finally, here is the template:
The key part is the jquery django-dynamic-formset plugin that keeps adding new inline forms:
<form id="father-form" method="POST" enctype="multipart/form-data" action=".">
{% csrf_token %}
<div class="row">
{% for f in form %}
<div class="span3">{{ f.label }}<br />{{ f }}
{% if f.errors %}
{% for v in f.errors %}
<br /><span style="color:red;">{{ v }}</span>
{% endfor %}
{% endif %}
</div>
{% endfor %}
</div>
<hr />
<h2>Sons:</h2>
<table class="table-striped">
<table>
{% for f2 in inlines %}
<tr id="{{ f2.prefix }}-row">
{% for i in f2 %}
<td>
{{ i }}{% if i.errors %}<span style="color:red;">{{ i.errors }}</span>{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
{{ inlines.management_form }}
<input type="submit" class="btn btn-primary" value="Go Go Gadget →">
</form>
<script type="text/javascript">
$(function() {
$('#father-form tr').formset({
prefix: '{{ inlines.prefix }}'
});
})
</script>

Categories