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.
Related
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();
}
}
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!
Apologies for the long post, I am trying to implement a simple button which either add or remove an item from a watchlist.
While I initially managed to implement the "addwatchlist" function appropriately, I tinkered my code for a few hours and I somewhat am unable to wrap my head around it again.
Here is the error that I receive when pressing the "Add to Watchlist" button :
TypeError at /addwatchlist/10
Field 'id' expected a number but got <Listing: "Gloss + Repair Spray">.
Request Method: GET
Request URL: http://127.0.0.1:8000/addwatchlist/10
Django Version: 3.1.1
Exception Type: TypeError
Exception Value:
Field 'id' expected a number but got <Listing: "Gloss + Repair Spray">.
TRACEBACK :
watchlist.save()
▼ Local vars
Variable Value
id 10
request <WSGIRequest: GET '/addwatchlist/10'>
watchlist Error in formatting: TypeError: Field 'id' expected a number but got <Listing: "Gloss + Repair Spray">.
Here is the error that I receive when pressing the "Remove from Watchlist" button, note that this is the exact same error I initially received which in turn forced me to try and tweak the way "Add to Watchlist" function :
AssertionError at /removewatchlist/1
Watchlist object can't be deleted because its id attribute is set to None.
Request Method: GET
Request URL: http://127.0.0.1:8000/removewatchlist/1
Django Version: 3.1.1
Exception Type: AssertionError
Exception Value:
Watchlist object can't be deleted because its id attribute is set to None.
TRACEBACK :
watchlist.delete()
▼ Local vars
Variable Value
id 1
request <WSGIRequest: GET '/removewatchlist/1'>
watchlist <Watchlist: 1 Watchlist ID# None : admin watchlisted : "Iphone 11 Pro">
Models.py :
class Listing(models.Model):
owner = models.CharField(max_length=64)
title = models.CharField(max_length=64)
description = models.TextField(max_length=1024)
startingBid = models.IntegerField()
link = models.URLField(blank=True)
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="category_id")
time = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(null=False, default='True')
def __str__(self):
return f'"{self.title}"'
class Watchlist(models.Model):
user = models.CharField(max_length=64)
watchlist_listingid = models.ForeignKey(Listing, on_delete=models.CASCADE, related_name="watchlist_listingid", null=True)
def __str__(self):
return f'{self.watchlist_listingid_id} Watchlist ID# {self.id} : {self.user} watchlisted : {self.watchlist_listingid}'
Views.py :
#login_required
def watchlist(request):
if request.user.username:
return render(request, "auctions/watchlist.html", {
"items" : Watchlist.objects.filter(user=request.user.username),
"watchlist_listingid" : Watchlist.objects.values_list('watchlist_listingid', flat=True).filter(user=request.user.username),
"watchlist_count" : len(Watchlist.objects.filter(user=request.user.username))
})
def addwatchlist(request, id):
if request.user.username:
watchlist = Watchlist(user=request.user.username,watchlist_listingid_id=Listing.objects.filter(id=id).first())
watchlist.save()
return redirect('listing', id=id)
else:
return render('auctions/watchlist.html', {})
def removewatchlist(request, id):
if request.user.username:
# all_entries = Watchlist.objects.values_list('watchlist_listingid', flat=True).filter(user='admin')
# for i in all_entries:
# if i == id:
# removeMe = i
# else:
# removeMe = 'TEST ERROR'
watchlist = Watchlist(user=request.user.username,watchlist_listingid_id=id)
watchlist.delete()
return redirect('listing', id=id)
Urls.py :
path("listing/<int:id>", views.listing, name="listing"),
path("watchlist", views.watchlist, name="watchlist"),
path("addwatchlist/<int:id>", views.addwatchlist, name="addwatchlist"),
path("removewatchlist/<int:id>", views.removewatchlist, name="removewatchlist")
The Add/Remove buttons are hosted on listing.html :
{% if user.is_authenticated %}
<p>
{% if watchlisted %}
<button class="btn btn-danger">Remove from watchlist</button>
{% else %}
<button class="btn btn-success">Add to watchlist</button>
{% endif %}
</p>
{% endif %}
Questions :
Am I complicating myself by using Watchlist.watchlist_listingid as a
foreign key for Listing? I have seen quite a few other posts where
people would tend not to use foreign keys at all, though I believe it
might be not optimized.
I could eventually be using a form to envelop those buttons but I
already have a form in my listing.html page (to add comments).
Tackling the data from a form might be easier though I have not
managed to find out how to implement several distinct Django forms on
one HTML page. What do you consider best practice? Forms or direct
link summoning functions?
For "Add to Watchlist", why do I obtain def __str__(self):return f'"{self.title}"'instead of simply adding a new entry in my
Watchlist table?
Finally, for "Remove from Watchlist",why do i receive "id attribute is set to None" when I know for a fact that on my example,the
Listing.id for "Iphone 11 Pro" is 1, and technically speaking,
Watchlist.id (which does not show at all in my model) is 11 based on
the Django Admin page. Even hardcoding 11 to force its deletion off
the Watchlist table still returns this "none" error.
Looking forward to your replies!
Per your specific error, the problem is in this line, which should be clear from the error message:
watchlist = Watchlist(user=request.user.username,watchlist_listingid_id=Listing.objects.filter(id=id).first())
id is expecting an int (the id of a Model instance), but you are actually passing it an object instance of your Listing model; filter() returns a queryset, and then .first() returns the first object in that queryset.
That's why the error is telling you that "field id", which expects an int, is instead getting "<Listing: "Gloss + Repair Spray">" -- that's an actual instance of your Listing model.
Solution:
Simply adding .id after the .first() should fix your problem, so you are actually passing the id of that object to the id field:
watchlist = Watchlist(user=request.user.username,watchlist_listingid_id=Listing.objects.filter(id=id).first().id)
That should resolve your specific issue; having said that, though, I highly recommend you use forms for these buttons. Currently, you are allowing modification of the contents of your database in an insecure fashion. Any activity that modifies database content should be delivered as a POST request. You can do this with forms quite easily, even if you aren't using any actual input-fields. The button will submit the empty form as a POST request, you can include a csrf token in your form tag in the template, and the db will be modified accordingly. Another option, if you really don't want to use forms, is to use AJAX calls (via jQuery), which can be sent as POST requests without the use of forms. Explaining how to do that is beyond the scope of this response, but there is plenty of info here on SO.
Lastly, if a Watchlist is tied to a User, you should probably consider a different database schema entirely for getting results-- that is, one that actually ties the User model to the item they are adding to their watchlist (it's possible, though, that I'm misunderstanding your intent here, and maybe you don't want that). You could connect the User model and the Listing models via a Many-to-Many relationship, since a User can 'watchlist' many listings, and a listing can be 'watchlisted' by many users.
Fix for : "Field 'id' expected a number but got <Listing: "Gloss + Repair Spray">."
Simply add .id after .first()
watchlist = Watchlist(user=request.user.username,watchlist_listingid_id=Listing.objects.filter(id=id).first() = <Listing: "Gloss + Repair Spray">
watchlist = Watchlist(user=request.user.username,watchlist_listingid_id=Listing.objects.filter(id=id).first().id) = 6 (which is the ID in my database)
Fix for : Watchlist object can't be deleted because its id attribute is set to None.
watchlist = Watchlist.objects.get(user=request.user.username, watchlist_listingid=id)
GET the whole row in your table and assign it to a variable before using delete()
Instead of the faulty :
watchlist = Watchlist(user=request.user.username,watchlist_listingid_id=id)
Finally, I found out that to use different Django Forms in the same HTML page, you can set the action of the form to a path url in URLS.PY which would then trigger a special functions in Views.py.
I am trying to create a new model instance every time a url is accessed. so far, I have the function working in my views.py, but when the new model instance is created, the fields are empty (because I have not specified what I'd like in those fields in views.)
views.py
def session_invent(self):
session = Session() # I can add field data in here, but I want to get it via the URL
session.save()
messages.success(self, f'session invented!')
return redirect('blog-home')
urls.py
path('session/invent/', views.session_invent, name="session-invent"),
models.py
class Session(models.Model):
uid = models.CharField(max_length=50)
cid = models.CharField(max_length=50)
qid = models.CharField(max_length=50)
aid = models.CharField(max_length=50)
session_date = models.DateTimeField(auto_now_add=True)
def qid_plus_aid(self):
return '{}_{}'.format(self.qid, self.aid)
def __str__(self):
return self.uid
def get_absolute_url(self):
return reverse('session-detail', kwargs={'pk': self.pk})
Ok, so here is what i am trying to pull off:
right now if i enter mywebsite.com/session/invent/ a new Session model instance is created with empty fields. Is there a way I can fill in those fields with args in the URL? For example, something like...
mywebsite.com/session/invent/?uid=test_uid&cid=test_cid&qid=test_qid&aid=test_aid
Finished Answered code:
From the answer below here is how the updated views.py should look:
def session_invent(request):
session = Session.objects.create(
uid=request.GET['uid'],
cid=request.GET['cid'],
qid=request.GET['qid'],
aid=request.GET['aid']
)
messages.success(request, f'session invented from URL!')
return redirect('blog-home')
So, If I enter the following URL, a new record is created in my database with the values in each field set from the URL:
mywebsite.com/session/invent/?uid=test_uid&cid=test_cid&qid=test_qid&aid=test_aidz
Yes, the parameters are stored in the querystring, and you can use request.GET for a dictionary-like representation of the querystring, so:
def session_invent(request):
session = Session.objects.create(
uid=request.GET['uid'],
cid=request.GET['cid'],
qid=request.GET['qid'],
aid=request.GET['aid']
)
messages.success(request, f'session invented!')
return redirect('blog-home')
This will raise a HTTP 500 in case one of the keys is missing in the request.GET. You can use request.GET.get(…) [Django-doc] to access an element with an optional default value.
A GET request is however not supposed to have side effects. It is furthermore quite odd for a POST request to have a querystring.
When I redirect to another view after adding data, specifically stripe Customer data, to a dict that is then added to my session, I lose all of the information in my session at the redirected view. Thus, I encounter a KeyError when I try to pop these items.
Interestingly, this does not happen when I put other types of information in my payment_data dict, like a list instead of a customer object.
I'm not sure what's the best way to fix this problem, but given what I have designed, it's important for me to get the Customer information to the confirm view so that I can
List item
Display customer information to the user for confirmation (censoring sensitive information
Charge the card
This is my code:
class PaymentsCreateView(FormView):
def form_valid(self, form):
customer = stripe.Customer.create(description="""
Non-registered user for applying features""")
customer.save()
payment_data = {
'customer': customer
}
self.request.session['payment_data'] = payment_data
self.request.session.modified = True
import ipdb;ipdb.set_trace();
return HttpResponseRedirect(reverse('payments_confirm'))
class PaymentsConfirmView(TemplateView):
template_name = 'payments/confirm.html'
def get_context_data(self, **kwargs):
context = super(PaymentsConfirmView, self).get_context_data(**kwargs)
context['payment_data'] = self.request.session.pop('payment_data')
context['feature_data'] = self.request.session.pop('feature_data')
return context
I'm still debugging and my next step is to confirm whether the issue is that I am trying to store a Customer object rather than a dictionary or list object but maybe someone on SO can confirm or supply the right answer.
From the Python docs:
list.pop([i])
Remove the item at the given position in the list, and return it. If no index is specified, a.pop() removes and returns the last item in the list.
Like Rohan says, use get():
context['payment_data'] = self.request.session.get('payment_data', False)
context['feature_data'] = self.request.session.get('feature_data', False)