I want to show one button if a user has already added an object, and another button if he/she hasn't.
In my template I have:
<tr>
{% for word in dict_list %}
<td>{{word.target_word}} </td>
<td>{{word.source_word}}</td>
<td>
Add
{% if user_word %}
<a href="" class="add-word btn btn-success btn-sm" >Added</a>
{% endif %}
</td>
</tr>
{% endfor %}
And in my views:
def custom_create_word(request, object):
if request.method == 'POST':
pass
if request.method =="GET":
from .forms import WordForm
from .models import Word
word = Word.objects.get(pk=object)
user = request.user
target_word = word.target_word
source_word = word.source_word
deck_name = "My Words"
fluency = 0
new_word, created = Word.objects.get_or_create(user=user, target_word=target_word,
source_word=source_word, deck_name=deck_name, fluency=fluency)
return HttpResponseRedirect(reverse('vocab:dict'))
def get_context_data(self,**kwargs):
context = super(custom_create_word, self).get_context_data(**kwargs)
if Word.objects.filter(target_word=target_word, user=user).exists():
user_word == True
context['user_word'] = user_word
return context
I don't get any errors, but I don't get the desired result either. Am I going about it the wrong way?
Updated template:
<tbody>
<tr>
{% for word in dict_list %}
<td>{{word.target_word}}</td>
<td>{{word.source_word}}</td>
<td>
{% if user_word %}
<a href="" class="btn btn-success btn-sm" >Added</a>
{% else %}
Add
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
Updated views:
class Dictionary(FilterView):
model = Word
template_name = 'vocab/dictionary.html'
context_object_name = 'dict_list'
paginate_by = 15
filterset_class = WordFilter
strict = False
def get_queryset(self):
qs = self.model.objects.filter(user__username__iexact='special_user')
return qs
def get_object(self):
queryset = qs
pk = self.kwargs.get('pk')
if pk is None:
raise AttributeError('pk expected in url')
return get_object_or_404(queryset, pk=pk)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filter'] = WordFilter(self.request.GET, queryset=self.get_queryset())
word = Word.objects.get(pk=self.kwargs.get('pk'))
target_word = word.target_word
context['user_word'] = Word.objects.filter(target_word=target_word, user=self.request.user).exists()
return context
Update: If I hard code the pk in the above, I get the desired result, i.e. Add if word doesn't exist, and Added if it does. But the pk=self.kwargs.get('pk') doesn't work, I get DoesNotExist error - what should it be? How can I get access to each object's pk in the ListView?
urls.py:
app_name='vocab'
urlpatterns = [
path("index/",views.VocabHome.as_view(),name='index'),
path("list/", views.WordList.as_view(), name="list"),
path("create/", views.CreateWord.as_view(), name="create"),
re_path(r"by/(?P<username>[-\w]+)/(?P<pk>\d+)/$",views.WordDetail.as_view(),name="detail"),
re_path(r"delete/(?P<pk>\d+)/$",views.WordDelete.as_view(),name="delete"),
re_path(r"edit/(?P<pk>\d+)/$",views.WordUpdate.as_view(),name="edit"),
path('dictionary/', views.Dictionary.as_view(),name='dict'),
path("<int:object>/",views.custom_create_word,name="add-custom"),
]
I've also now tried the following:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filter'] = WordFilter(self.request.GET, queryset=self.get_queryset())
special_user_word = Word.objects.filter(user__username__iexact='special_user', target_word='target_word')
logged_user_word = Word.objects.filter(user=self.request.user, target_word='target_word')
user_word = None
if special_user_word == logged_user_word:
user_word = True
context['user_word'] = user_word
return context
But I get None everywhere... any thoughts?
Can you share the url's file, in the updated view i don't see how the function is getting called and which function is called.
It's user_word = True, not user_word == True.
Also in your template you should use the else clause otherwise when user_word is True you'll have 2 buttons.
Finally, def get_context_data(self,**kwargs): will not be executed because you are using a function as a view. get_context_data is for generic class based views. And even if it were executed, which I guess it's not, target_word that you use in it is not defined.
Update for you new problem
replace
word = Word.objects.get(pk=self.kwargs.get('pk'))
with
word = Word.objects.get(pk=kwargs.get('pk'))
Related
I have a Dictionary view that shows the list of words created by a specific (special) user:
class Dictionary(FilterView):
model = Word
template_name = 'vocab/dictionary.html'
context_object_name = 'dict_list'
paginate_by = 15
filterset_class = WordFilter
strict = False
def get_queryset(self):
qs = self.model.objects.filter(user__username__iexact='special_user')
return qs
def get_object(self):
queryset = qs
pk = self.kwargs.get('pk')
if pk is None:
raise AttributeError('pk expected in url')
return get_object_or_404(queryset, pk=pk)
Now I want any user to be able to come to this page and add any word that they want to, like this:
def custom_create_word(request, object):
if request.method == 'POST':
pass
if request.method =="GET":
from .forms import WordForm
from .models import Word
word = Word.objects.get(pk=object)
user = request.user
target_word = word.target_word
source_word = word.source_word
deck_name = "My Words"
fluency = 0
new_word, created = Word.objects.get_or_create(user=user, target_word=target_word,
source_word=source_word, deck_name=deck_name, fluency=fluency)
return HttpResponseRedirect(reverse('vocab:dict'))
Everything works as expected. But in the template I want the button to look different depending on whether the logged in user already has this word in their own list (which should be judged by if target_word is the same). My template looks like this:
<tr>
{% for word in dict_list %}
<td>{{word.target_word}}</td>
<td>{{word.source_word}}</td>
<td>
{% if user_word %}
<a href="" class="btn btn-success btn-sm" >Added</a>
{% else %}
Add
{% endif %}
</td>
</tr>
{% endfor %}
The way I thought about doing this is to overwrite get_context_data on my Dictionary view so that I can check if the logged in user's target_word is equal to the special user's target_word, and to pass this into context. So my view is:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filter'] = WordFilter(self.request.GET, queryset=self.get_queryset())
special_user_word = Word.objects.filter(user__username__iexact='special_user', target_word='target_word')
logged_user_word = Word.objects.filter(user=self.request.user, target_word='target_word')
user_word = None
if special_user_word == logged_user_word:
user_word = True
context['user_word'] = user_word
return context
But I get None everywhere... any thoughts?
Obviously they won't be same, because the Word objects are totally different as they are created differently for each user inside custom_create_word. Also, user_word won't work for all the words, you need provide it for each word. You can override the get_queryset method like this(using conditional expression):
from django.db.models import Value, Case, When, BooleanField
class WordListView(...):
...
def get_queryset(self, **kwargs):
queryset = super().get_queryset(**kwargs)
special_user_word = Word.objects.filter(user__username__iexact='special_user', target_word='target_word').values('source_word', flat=True)
return queryset.annotate(
user_word=Case(
When(source_word__in=special_user_word, then=Value(True)),
default=Value(False),
output_field=BoolenField()
)
)
Usage in template:
{% for word in dict_list %}
{% if word.user_word %}
already added
{% else %}
add
{% endif %}
{% endfor %}
Finally got it to work!
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filter'] = WordFilter(self.request.GET, queryset=self.get_queryset())
special_user_word = Word.objects.filter(user__username__iexact='special_user').values_list('target_word', flat=True)
logged_user_word = Word.objects.filter(user=self.request.user).values_list('target_word', flat=True)
user_word = list(set(special_user_word) & set(logged_user_word))
context['user_word'] = user_word
return context
And in my template:
{% if word.target_word in user_word %}
So I am trying to allow users to edit menu item prices that are in the database currently. The fields will auto-populate data into the page and users will be able to edit that data to change the price. Here is what I have. I have tried and asked many questions, but I am still lost. I have googled a lot and it helped me understand forms a bit, but I'm not able to fix it. Please let me know if you need more info.
Views.py:
def edit_menu(request):
queryset = Product.objects.all()
context = { "object_list": queryset }
if request.method == 'POST':
post=ProductModelForm(request.POST)
if request.POST.get('price') and request.POST.get('name'):
if 'name' == Product.name:
post.name= request.POST.get('name')
post.price= request.POST.get('price')
post.save()
return redirect('Edit Menu Item')
else:
return redirect('Edit Menu Item')
else:
return render(request, 'mis446/edit-menu-item.html', context)
else:
return render(request, 'mis446/edit-menu-item.html', context)
forms.py:
class ProductModelForm(forms.ModelForm):
class Meta:
model = Product
fields = ['name','price'] # specify which field you need to be in the form
HTML:
<title>ACRMS - Edit Menu Price</title>
<div class = "container">
<form action = "" method = 'POST'>
{% csrf_token %}
{% for instance in object_list %}
<input name = "name" value = "{{ instance.name }}"></input>
<input type="number" name="price" value = "{{ instance.price }}"/><br>
{% endfor %}
</select>
<button type ="submit">Submit Changes</button>
</form>
</div>
Urls.py:
url('edit-menu/edit/',views.edit_menu, name='Edit Menu Item'),
models.py:
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.IntegerField()
slug = models.SlugField()
def __str__(self):
return self.name
For your current implementation, you do not need a form. Instead, update the view like this:
# view
def edit_single_menu(request, pk):
if request.method == 'POST':
post=Product.objects.get(pk=pk)
if request.POST.get('price') and request.POST.get('name'):
post.name= request.POST.get('name')
post.price= request.POST.get('price')
post.save()
return redirect('Edit Menu Item')
else:
return redirect('Edit Menu Item')
return render(request, 'mis446/edit-menu-item.html', context)
# url
url('edit-menu/edit/<pk:id>/',views.edit_single_menu, name='edit_single_menu'),
# template (mis446/edit-menu-item.html)
<title>ACRMS - Edit Menu Price</title>
<div class = "container">
{% for instance in object_list %}
<form action = "{% url 'edit_single_menu' instance.pk %}" method = 'POST'>
{% csrf_token %}
<input name = "name" value = "{{ instance.name }}"></input>
<input type="number" name="price" value = "{{ instance.price }}"/><br>
<button type ="submit">Submit Changes</button>
</form>
{% endfor %}
</div>
Here I am sending individual edit to a new separated view named edit_single_menu and store the changes there.
Update
New url is not meant to replace the old one. It is only to assist you to update individual product. So, you need to keep both of the urls. Also, here is an answer based on #brunodesthuilliers's suggestion:
# view
def edit_single_menu(request, pk):
if request.method == 'POST':
post=Product.objects.get(pk=pk)
form = ProductForm(request.POST, instance=post)
if form.is_valid():
form.save()
return redirect('Edit Menu Item')
Also, do some changes on edit_menu view:
def edit_menu(request):
queryset = Product.objects.all()
context = { "object_list": queryset }
return render(request, 'mis446/edit-menu-item.html', context)
And urls should look like this:
from django.urls import include, path
# rest of the code
path('edit-menu/edit/<int:pk>/',views.edit_single_menu, name='edit_single_menu'),
path('edit-menu/edit/',views.edit_menu, name='Edit Menu Item'),
I have a category dropdown list in a template. I don't want to hard code it and write separate list views for each category. So is there a way to pass a value from <a href=""> to a view? I guess self.request.GET.get('category search') never works because there is no <form method="get"> tag. Therefor it always returns None due to the return queryset.none() statement.
home.html:
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
{% if all_categories %}
{% for category in all_categories %}
<a name="category search" href="{% url 'book:category_search' category.id %}"> {{ category }} </a>
{% endfor %}
{% endif %}
</div>
book.urls.py:
urlpatterns = [
.........
path('categories/<int:pk>', views.CategorySearchView.as_view(), name='category_search'),
]
book.views.py:
class CategorySearchView(generic.ListView):
template_name = 'book/search.html'
model = Book
context_object_name = 'book_list'
paginate_by = 100
def get_queryset(self):
queryset = super().get_queryset()
search = self.request.GET.get('category search')
if search:
queryset = Book.objects.filter(categories__id=search)
return queryset
else:
return queryset.none()
Since your URL has <int:pk>,
path('categories/<int:pk>', views.CategorySearchView.as_view(), name='category_search'),
you can access self.kwargs['pk'] in the view:
def get_queryset(self):
queryset = super().get_queryset()
search = self.kwargs.get('pk')
if search:
queryset = Book.objects.filter(categories__id=search)
return queryset
else:
return queryset.none()
As an aside, you would use self.request.GET if pk was in the querystring, e.g. /categories/?pk=1. When you submit a form with method=GET, the form fields will be included in the querystring. But you can also have a link with a querystring, e.g. my link.
Edit:
My goal is to create a small e-commerce.
In my index I have a list of products, one of the attributes is a boolean called in_cart which states if the product is in the cart or not.
By default all boolean values are false. In my template is a table with all the products next to which I put a button "add to cart", which redirects to the cart template. However when I click on add to cart, the value of the boolean in question does not change to true. Any thoughts?
<table>
<tr>
<th>List of car parts available:</th>
</tr>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
{% for product in products_list %}
<tr>
<td>{{ product.id }}</td>
<td>{{ product.name }}</td>
<td>${{ product.price }}</td>
<td>{% if not product.in_cart %}
<form action="{% url 'add_to_cart' product_id=product.id %}" method="POST">
{% csrf_token %}
<input type="submit" id="{{ button_id }}" value="Add to cart">
</form>
{% else %}
{{ print }}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
See cart
And these are my views:
def index(request):
if request.method == "GET":
products_list = Product.objects.all()
template = loader.get_template('products/index.html')
context = {'products_list': products_list}
return HttpResponse(template.render(context, request))
return HttpResponse('Method not allowed', status=405)
def cart(request):
cart_list = Product.objects.filter(in_cart = True)
template_cart = loader.get_template('cart/cart.html')
context = {'cart_list': cart_list}
return HttpResponse(template_cart.render(context, request))
def add_to_cart(request, product_id):
if request.method == 'POST':
try:
product = Product.objects.get(pk=product_id)
product.in_cart = True
product.save()
except Product.DoesNotExist:
return HttpResponse('Product not found', status=404)
except Exception:
return HttpResponse('Internal Error', status=500)
return HttpResponse('Method not allowed', status=405)
Model:
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.IntegerField()
in_cart = models.BooleanField(default=False)
ordered = models.BooleanField(default=False)
def __str__(self):
return self.name
URLs
urlpatterns = [
path('', views.index, name='index'),
path('cart/', views.cart, name='cart')
re_path(r'^add_to_cart/(?P<product_id>[0-9]+)$', views.add_to_cart, name='add_to_cart')
]
Error in my terminal
File "/Users/Nicolas/code/nicobarakat/labelachallenge/products/urls.py", line 8
re_path(r'^add_to_cart/(?P<product_id>[0-9]+)$', views.add_to_cart, name='add_to_cart')
^
SyntaxError: invalid syntax
This is what it looks like in localhost:
First of all, your if statement is not reachable. Because you had a return before it. When you call return in a function, the next lines of function those are after the return will not execute.
So you should change the index function.
Also, you should send an identifier of the product with your post request. Identifier could be the id or any other unique field in your model.
So your code should be something like this:
def index(request):
if request.method == 'POST': # Request is post and you want to update a product.
try:
product = Product.objects.get(unique_field=request.POST.get("identifier")) # You should chnage `unique_field` with your unique filed name in the model and change `identifier` with the product identifier name in your form.
product.in_cart = True
product.save()
return HttpResponse('', status=200)
except Product.DoesNotExist: # There is no product with that identifier in your database. So a 404 response should return.
return HttpResponse('Product not found', status=404)
except Exception: # Other exceptions happened while you trying saving your model. you can add mor specific exception handeling codes here.
return HttpResponse('Internal Error', status=500)
elif request.method == "GET": # Request is get and you want to render template.
products_list = Product.objects.all()
template = loader.get_template('products/index.html')
context = {'products_list': products_list}
return HttpResponse(template.render(context, request))
return HttpResponse('Method not allowed', status=405) # Request is not POST or GET, So we should not allow it.
I added all information you need in the comments of the code. I think you should spend more time on python and django documents. But if you still have any questions, you can ask in comments.
After question edit
If you don't want to use a readonly field in your form, you should make two changes in your code.
First of all, you should add a url with product_id parameter in your urls.py file. Something like this:
url(r'^add_to_cart/(?P<product_id>[0-9]+)$', 'add_to_cart_view', name='add_to_cart')
Then you should add separate your add_to_cart view from index view. Your views should be like this:
def index(request):
if request.method == "GET":
products_list = Product.objects.all()
template = loader.get_template('products/index.html')
context = {'products_list': products_list}
return HttpResponse(template.render(context, request))
return HttpResponse('Method not allowed', status=405)
def cart(request):
cart_list = Product.objects.filter(in_cart = True)
template_cart = loader.get_template('cart/cart.html')
context = {'cart_list': cart_list}
return HttpResponse(template_cart.render(context, request))
def add_to_cart(request, product_id):
if request.method == 'POST':
try:
product = Product.objects.get(pk=product_id)
product.in_cart = True
product.save()
return HttpResponse('', status=200)
except Product.DoesNotExist:
return HttpResponse('Product not found', status=404)
except Exception:
return HttpResponse('Internal Error', status=500)
return HttpResponse('Method not allowed', status=405)
Now you should change the action link of your form to this:
{% url 'add_to_cart' product_id=product.id %}
My aim is to use Two models in One template. I have tried various ways around this and have had no success. Originally I had 2 views, 2 models and Two forms. After searching I found people using inlineformsets. So I dropped One of the Views and set-up the inlineformset.
This is currently where I am up to and seem to be hitting a wall.
The template renders to the browser and the 'object_list' part displays the database content as desired and the 'form' part renders the form and validates/saves the data correctly. The issue is with the 'formset'. No fields are rendered (I would expect to see a dropdown as the field is a foreignkey) and when the 'submit' button is pressed I get:
AttributeError at /settings/
'NoneType' object has no attribute 'save'
Any help in finding the error or pointers on alternative solutions would be greatly appreciated.
The Code:
models.py
from django.db import models
class RevisionSettings(models.Model):
global_revision_type = models.CharField(max_length = 5, unique=True, blank = True)
global_revision_description = models.CharField(max_length = 300, unique=True, blank = True)
class Meta:
ordering = ["global_revision_type"]
def __unicode__(self):
return u'%s %s' % (self.global_revision_type, self.global_revision_description)
class RevisionDefaultType(models.Model):
defaultrevisiontype = models.ForeignKey(RevisionSettings)
class Meta:
ordering = ["defaultrevisiontype"]
def __unicode__(self):
return unicode(self.defaultrevisiontype)
views.py
class RevisionSettingsView(CreateView):
template_name = 'settings/revisionsettings_view.html'
model = RevisionSettings
form_class = SettingsForm
success_url = reverse_lazy('globalsettings')
success_message = 'Successfully added your new revision type'
def get(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = SettingsFormSet(instance = RevisionSettings)
return self.render_to_response(
self.get_context_data(form=form,
formset=formset))
def post(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = SettingsFormSet(self.request.POST)
if 'rev_settings_form_1' in self.request.POST:
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
elif 'rev_settings_form_2' in self.request.POST:
if formset.is_valid():
return self.formset_valid(formset)
else:
return self.form_invalid(formset)
def form_valid(self, form):
self.object = form.save()
self.object.save()
return HttpResponseRedirect(self.get_success_url())
def formset_valid(self, formset):
self.object.save()
formset.instance = self.object
formset.save()
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, form, formset):
return self.render_to_response(self.get_context_data(form=form,formset=formset))
def get_context_data(self, **kwargs):
kwargs['object_list'] = RevisionSettings.objects.order_by('global_revision_type')
return super(RevisionSettingsView, self).get_context_data(**kwargs)
forms.py
from django import forms
from django.forms.models import inlineformset_factory
from .models import RevisionSettings, RevisionDefaultType
class SettingsForm(forms.ModelForm):
class Meta:
model = RevisionSettings
class DefaultSettingsForm(forms.ModelForm):
class Meta:
model = RevisionDefaultType
SettingsFormSet = inlineformset_factory(RevisionSettings, RevisionDefaultType)
revisionsettings_view.html
(I have removed most of the HTML styling to keep the information to the point)
{% extends 'base_private.html' %}
{% block content %}
{% for object in object_list %}
<tr>
<td align="center">{{ object.global_revision_type }}</td>
<td align="center">{{ object.global_revision_description }}</td>
<td align="center"><span class="glyphicon glyphicon-remove-circle"></span></td>
</tr>
{% endfor %}
<form action = '{{ action }}' method = 'POST' class="form-horizontal" role="form">
{% csrf_token %}
<tr>
<td align="center">
{{ formset.management_form }}
{% for form in formset %}
{{ form.id }}
{{ form.defaultrevisiontype.label_tag }}
{{ form.defaultrevisiontype }}
{% endfor %}
</td>
</tr>
<span class="input-group-addon">
<input type = 'submit' name = 'rev_settings_form_2' value = 'Update Default Revision Type' class = 'btn btn-success'>
</span>
<td align="center">{{ form.global_revision_type }}{{ form.global_revision_type.errors }}</td>
<td align="center">{{ form.global_revision_description }}{{ form.global_revision_description.errors }}</td>
</tr>
<span class="input-group-addon">
<input type = 'submit' name = 'rev_settings_form_1' value = 'Add Revision Type' class = 'btn btn-success'>
</span>
</form>
{% endblock %}
Formsets are overkill for two forms. This is actually not too hard but poorly documented. You can make both forms the same form type, just give a prefix.
def parent_apply(request):
if request.method == 'POST':
parent_form = SignupForm(request.POST, prefix="parent")
student_form = StudentApplyForm(request.POST, prefix="student")
if parent_form.is_valid() and student_form.is_valid():
parent = parent_form.save()
student = student_form.save(parent)
else: messages.error(request, "Please correct the errors marked in red.")
else:
parent_form = SignupForm(prefix="parent")
student_form = StudentApplyForm(prefix="student")
return render_to_response('template_path/forms.html', { 'parent_form':parent_form, 'student_form':student_form }, context_instance=RequestContext(request))
The forms are just regular Django forms, no special settings required. You can change the order on which they validate and save one even if the other did not validate if you choose.
In your HTML Template, wrap both forms in the same tag and they will submit at the same time. If you want your forms to go to different view functions, specify two different elements.
Thanks for all the help. The pointers really helped me come to this solution. The main change was to 'def get' as shown below. I dropped the formset and passed the forms this way.
def get(self, request, *args, **kwargs):
form = self.settings_form_class
formset = self.default_form_class
return self.render_to_response(self.get_context_data(form = form, formset = formset))
I was unaware this was possible! Thanks again.