(django) Showing posts according to interests of users - python

I am pretty new to Django and still learning it, but I have to create something like medium.com. I want to show posts to users according to their interests. I added an interests field with a checkbox in a sign-up form. And of course, I added a category to the Post model. So, how can I show (to logged-in users only) publications that they interested in?
Here is my models.py file in posts app
from django.db import models
from django.utils import timezone
from django.urls import reverse
# Create your models here.
class Post(models.Model):
CATEGORY = (
('sport', 'Sport'),
('science', 'Science'),
('it', 'IT'),
('art', "Art"),
)
title = models.CharField(verbose_name="Title for publication", max_length=50)
category = models.CharField(verbose_name="Category", choices=CATEGORY, max_length=50)
author = models.ForeignKey("accounts.User", verbose_name="Author:", on_delete=models.CASCADE)
body = models.TextField(verbose_name="Body for publication")
pub_date = models.DateTimeField(verbose_name="Date:", auto_now_add=True)
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
def get_absolute_url(self):
return reverse("post_detail", kwargs={"pk": self.pk})
def __str__(self):
return self.title
And here is my vies.py file in posts app
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from .models import Post
from django.urls import reverse_lazy
class PostListView(ListView):
model = Post
template_name = "index.html"
def get_queryset(self):
return Post.objects.order_by('-pub_date')
class PostDetailView(DetailView):
model = Post
template_name = "./posts/post_detail.html"
class PostCreateView(CreateView):
model = Post
template_name = "./posts/new_post.html"
fields = '__all__'
class PostUpdateView(UpdateView):
model = Post
template_name = "./posts/update_post.html"
fields = ['body', 'title', 'category']
class PostDeleteView(DeleteView):
model = Post
template_name = "./posts/delete_post.html"
success_url = reverse_lazy('index')
class SportPostListView(ListView):
model = Post
template_name = "./posts/sports.html"
class ITPostListView(ListView):
model = Post
template_name = "./posts/it.html"
class SciencePostListView(ListView):
model = Post
template_name = "./posts/ilm-fan.html"
class ArtPostListView(ListView):
model = Post
template_name = "./posts/sanat.html"
Here is my index.html file
{% extends 'base.html' %}
{% block content %}
{% if object_list %}
{% for post in object_list %}
<h1>{{ post.title }}</h1>
<p>{{ post.pub_date }}</p>
<p>{{ post.author }}</p>
<p>{{ post.body }}</p>
<p>{{ post.category }}</p>
{% endfor %}
{% else %}
<p>No posts are available.</p>
{% endif %}
{% endblock content %}
This is my forms.py file in accounts app
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.db import models
from django import forms
class SignUpForm(UserCreationForm):
INTERESTS = (
('sport', 'Sport'),
('science', 'Science'),
('it', 'IT'),
('art', "Art"),
)
username = forms.CharField(max_length=128, required=True)
email = models.EmailField(verbose_name='emailingiz', unique=True, default='')
first_name = forms.CharField(max_length=128, required=True)
last_name = forms.CharField(max_length=128, required=True)
interests = forms.MultipleChoiceField(required=True, widget=forms.CheckboxSelectMultiple, choices=INTERESTS)
USERNAME_FIELD = 'email'
class Meta:
model = User
fields = ('first_name', 'last_name', 'username', 'email', 'interests', 'password1', 'password2')
And lastly this views.py file in accounts app
from django.urls import reverse_lazy
from django.views.generic import CreateView
from .forms import SignUpForm
class SignUpView(CreateView):
form_class = SignUpForm
success_url = reverse_lazy('login')
template_name = 'signup.html'
I have no idea how to do this, but I know that I have to do something in the views.py file in the posts app, however, I do not know what to do.
Will be really grateful if you could help me.

I would make another model Category. Since it will be its own model, you will be able to add future categories on the fly if needed, rather than having those hardcoded choices.
class Category(models.Model):
name = models.CharField(default=None, blank=True, null=True)
Then your User and Post model will each have a ManytoMany field related to category. Posts can be tagged as certain categories when they are created, and Users will be able to select a number of categories that they are interested in and have them stored in this field:
class User(AbstractBaseUser):
interests = models.ManytoManyField(Category, default=None, blank=True)
class Post(models.Model):
categories = models.ManytoManyField(Category, default=None, blank=True)
You can make use of a forms.modelChoiceField which will enable your user to select a category.
In your PostListView, all you need to do is change the get_queryset method to filter for posts that the user likes.
class PostListView(ListView):
model = Post
template_name = "index.html"
def get_queryset(self):
user_interests = self.request.user.interests
return Post.objects.filter(
categories__in=user_interests).order_by('pub_date')
Then, you should get the posts that share categories with the user's interests.

Here is the error
Error during template rendering
In template /home/xesos/projects/mw/mw/templates/base.html, error at line 0
too many values to unpack (expected 2)
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <style>
7
8 </style>
9 <title>
10 {% block title %}
To avoid this error I did some changes in forms.py, here is it
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django import forms
from posts.models import Category
class SignUpForm(UserCreationForm):
categories = Category.objects.values_list('name')
listOfCategory = []
listOfCategory += categories
username = forms.CharField(max_length=128, required=True)
email = forms.EmailField(required=True)
first_name = forms.CharField(max_length=128, required=True)
last_name = forms.CharField(max_length=128, required=True)
USERNAME_FIELD = 'email'
for i in range(0, len(listOfCategory)):
listOfCategory.append(listOfCategory[i])
interests = forms.ChoiceField(choices=[(x, x) for x in listOfCategory])
class Meta:
model = User
fields = ('first_name', 'last_name', 'username', 'email', 'interests', 'password1', 'password2')

Related

Select a valid choice. That choice is not one of the available choices --Django forms

I am trying to make a drop down select form (category) in django. The form renders well on the webpage but when I try to submit I get the error Select a valid choice. That choice is not one of the available choices.
I have done everything possible within my capability to resolve this. if you have any idea on how to go about this please help.
model.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('detail', args=[str(self.id)])
# return reverse('home')
class Post(models.Model):
title = models.CharField(max_length=100)
text = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
edited = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now_add=True)
category = models.CharField(max_length=100, default='coding')
def __str__(self):
return f'{self.title} by {self.author} {self.pk}'
def get_absolute_url(self):
return reverse('detail', args=[str(self.id)])
# return reverse('home')
form.py
from django import forms
from .models import Post, Category
choices = Category.objects.all().values_list('name','name')
choices_list = []
for item in choices:
choices_list.append(item)
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ('title', 'category', 'author','text')
widgets={
'title': forms.TextInput(attrs={'class':'form-control'}),
'category': forms.Select(choices=choices_list, attrs={'class':'form-control'}),
'author': forms.TextInput(attrs={'class':'form-control', 'id':'author'}),
# 'author': forms.Select(attrs={'class':'form-control'}),
'text': forms.Textarea(attrs={'class':'form-control','placeholder':choices_list}),
}
class EditForm(forms.ModelForm):
class Meta:
model = Post
fields = ('title', 'text')
widgets={
'title': forms.TextInput(attrs={'class':'form-control'}),
'text': forms.Textarea(attrs={'class':'form-control','placeholder':"less than 500 words"}),
# 'author': forms.Select(attrs={'class':'form-control'})
}
views.py
class createarticleview(CreateView):
template_name='posts/addpost.html'
model = Post
form_class = PostForm
#fields = '__all__'
# fields = ('title','text') for certain fields
def get_context_data(self, *args, **kwargs):
cat_menu = Category.objects.all()
context = super(createarticleview, self).get_context_data(*args, **kwargs)
context['cat_menu'] = cat_menu
return context
addpost.html
{%extends 'index.html'%}
{%block content%}
{% if user.is_authenticated %}
<div class="container">
<h3>add post...!!!.{{user.username}}</h3>
<br>
<div class="mb-3">
<form method="POST"> {% csrf_token%}
{{form.as_p}}
<button type="submit" class="btn btn-info"> post</button>
</form>
</div>
</div>
<script>
var name = "{{user.username}}";
if(document.getElementById("author").value=name)
document.getElementById('author').readOnly = true;
</script>
{% else%}
<h3>you are not logged in</h3>
{%endif%}
{%endblock content%}
Firstly, Always use PascalCase while defining the name of class, like you can give CreateArticleView rather than createarticleview.
you haven't given choices while defining your model that is Post and given models.CharField().
Update your Post model with choices attribute.
Try this:
models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
CATEOGRY_TYPES = (
('sp', 'sport'),
('te', 'technology'),
('bu', 'business')
)
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('detail', args=[str(self.id)])
# return reverse('home')
class Post(models.Model):
title = models.CharField(max_length=100)
text = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
edited = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now_add=True)
category = models.CharField(
choices=CATEOGRY_TYPES, max_length=2, default='coding')
def __str__(self):
return f'{self.title} by {self.author} {self.pk}'
def get_absolute_url(self):
return reverse('detail', args=[str(self.id)])
# return reverse('home')
views.py
from django.shortcuts import render
from .models import Post, Category
from .forms import PostForm
from django.views.generic.edit import CreateView
class CreateArticleView(CreateView):
template_name = 'posts/addpost.html'
model = Post
form_class = PostForm
success_url = '/success/'
def success(req):
return render(req, 'posts/success.html')
Rest of things will be remain same.
You can do it without reverse method by direclty making ForeignKey field in your Post model.
you can also do this:
models.py
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length=100)
text = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
edited = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now_add=True)
category = models.ForeignKey(
Category, on_delete=models.CASCADE, default='coding')
def __str__(self):
return f'{self.title} by {self.author} {self.pk}'
views.py
from django.shortcuts import render
from .models import Post, Category
from .forms import PostForm
from django.views.generic.edit import CreateView
class CreateArticleView(CreateView):
template_name = 'posts/addpost.html'
model = Post
form_class = PostForm
success_url = '/success/'
def success(req):
return render(req, 'posts/success.html')
Your forms.py can be remain same.
Remember: choices while defining models will be given more preference than ForeignKey.

Why is the inline formset not validating

I have two models with foreign key relation
models.py
from django.db import models
# Create your models here.
class Project(models.Model):
STATUSES = (
('Ongoing', 'Ongoing'),
('Completed', 'Completed')
)
YEARS = (
(2019, 2019),
(2020, 2020),
(2021, 2021),
(2022, 2022)
)
name = models.CharField(max_length=200)
client = models.CharField(max_length=100)
year = models.SmallIntegerField(choices=YEARS)
status = models.CharField(max_length=10, choices=STATUSES)
picture = models.ImageField(blank=True, null=True)
description = models.TextField(blank=True, null=True)
def __str__(self):
return self.name
class Photo(models.Model):
project = models.ForeignKey("Project", on_delete=models.CASCADE, related_name="images", blank=True, null=True)
image = models.ImageField()
description = models.CharField(max_length=100, blank=True, null=True)
slide = models.BooleanField(default=False)
I want photos and project to be created on the same form so I've used inline_formset_factory
forms.py
from django.forms import inlineformset_factory
from projects.models import Photo, Project
from django.forms import ModelForm
class ProjectModelForm(ModelForm):
class Meta:
model = Project
fields = (
'name',
'client',
'year',
'picture',
'status',
'description',
)
class PhotoModelForm(ModelForm):
class Meta:
model = Photo
fields = (
'image',
'slide',
'description'
)
PhotoFormset = inlineformset_factory(Project, Photo, form=PhotoModelForm, extra=1)
I used the generic CreateView
views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from projects.forms import PhotoFormset, ProjectModelForm
from django.shortcuts import redirect, reverse, render
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from .models import Photo, Project
# Create your views here.
class ProjectCreateView(LoginRequiredMixin, CreateView):
template_name = 'projects/project_create.html'
form_class = ProjectModelForm
def get_context_data(self, **kwargs):
context = super(ProjectCreateView, self).get_context_data(**kwargs)
context['photos_formset'] = PhotoFormset()
return context
def post(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
photo_formset = PhotoFormset(self.request.POST)
if form.is_valid() and photo_formset.is_valid():
return self.form_valid(form, photo_formset)
else:
return self.form_invalid(form, photo_formset)
def form_valid(self, form, photo_formset):
self.object = form.save(commit=False)
self.object.save()
print('valid')
photos = photo_formset.save(commit=False)
for photo in photos:
photo.project = self.object
photo.save()
return reverse('projects:projectspage')
def form_invalid(self, form, photo_formset):
if not photo_formset.is_valid():
print('invalid formset')
return self.render_to_response(
self.get_context_data(form=form, photos_formset=photo_formset)
)
def get_success_url(self):
return reverse('projects:projectspage')
this is the template
{% extends 'base.html' %}
{% load crispy_forms_filters %}
<!-- crispy_forms_tags -->
{% block content %}
<div class="container">
<h2>create new project</h2>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<h2 class="display-6 my-5">
Add photos
</h2>
{{ photos_formset.as_p }}
<input type="submit" value="Create" class="btn btn-primary">
</form>
</div>
{% endblock content %}
When I submit form_invalid is returned from the post method of the view
How do i get the inline_formset to validate or is there better way of doing it
I prefer to write those views as functions, seems more straightforward, try:
#login_required
def post_news(request):
if request.method == 'POST':
project_form = ProjectModelForm(request.POST, request.FILES)
photo_form = PhotoModelForm(request.POST, request.FILES)
picture = request.FILES['picture']
image = request.FILES['image']
if project_form.is_valid() and photo_form.is_valid():
project_instance = project_form.save(commit=False)
project_instance.picture = picture
project_instance.save()
photo_instance = project_form.save(commit=False)
photo_instance.project = project_instance
photo_instance.image = image
photo_instance.save()
return reverse('projects:projectspage')
else:
project_form = ProjectModelForm()
photo_form = PhotoModelForm()
return render(request, 'projects/project_create.html',
{'project_form': project_form, 'photo_form': photo_form})
You will have to account for a case when no picture for the project model is is provided I think. Just pop both forms into your template and this should work.

How to pass checkbox field to forms in django

I know it might be a duplicate and or such a simple thing to ask here. Please assist if you know how to and ask for more details to help me sort my problem. DISCLAIMER: I am a django beginner.
I have a model which has a foreignkey field. I'd like to have the options passed into the foreignkey field display in checkboxes. I have not been able to get it work. These are what I have
class Students(models.Model):
school = models.ForeignKey(School, on_delete=models.CASCADE)
name = models.CharField(max_length=200,unique=True,help_text="Student's name")
form = models.ForeignKey(Form, on_delete=models.CASCADE)
class Form(models.Model):
school = models.ForeignKey(School, on_delete=models.CASCADE)
form = models.CharField(max_length=20)
All the options in form model are to be passed as check boxes in Students model
My forms.py is shown below
class StudentsForm(forms.ModelForm):
def __init__(self, school, *args, **kwargs):
super(StudentsForm, self).__init__(*args, **kwargs)
self.fields['form'] = forms.ModelChoiceField(
queryset=Form.objects.filter(school=school))
class Meta:
model = Students
fields = ("name",'form')
My views.py
class StudentView(LoginRequiredMixin,CreateView):
model = Students
form_class = StudentsForm
template_name = 'add_student.html'
success_url = reverse_lazy('students')
def get_form_kwargs(self):
kwargs = super(StudentView, self).get_form_kwargs()
kwargs['school'] = self.request.user.school
return kwargs
Could I have the options as checkboxes, please???
I would recommend using the default modelchoicefield for foreignkey relations which will give you a select box in your template. Javascript is not my best programming language but here's an answer on how to set this up:
models.py
from django.db import models
class School(models.Model):
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Students(models.Model):
school = models.ForeignKey(School, on_delete=models.CASCADE)
name = models.CharField(max_length=200, unique=True, help_text="Student's name")
def __str__(self):
return self.name
forms.py
from django import forms
from .models import Students
class StudentsForm(forms.ModelForm):
school = forms.CharField(widget=forms.HiddenInput)
class Meta:
model = Students
fields = ("name", "school")
views.py
from django.urls import reverse_lazy
from django.views.generic import FormView
from .forms import StudentsForm
from .models import Students, School
class StudentView(FormView):
template_name = 'add_student.html'
model = Students
form_class = StudentsForm
success_url = reverse_lazy('students')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['schools'] = School.objects.all()
return context
add_student.html
<form>
{{ form.as_p }}
<ul class="school__list">
{% for school in schools %}
<li class="school__list-item">{{ school.name }}
<input type="checkbox" class="school__list-item-checkbox"
value="{{ school.id }}" name="school"/>
</li>
{% endfor %}
</ul>
</form>
<script>
const schools = document.querySelectorAll('.school__list-item-checkbox');
for (let i = 0; i < schools.length; i++) {
schools[i].addEventListener('click', () => {
{# populate the hidden school field with the value that is checked here. #}
})
}
</script>

Django version 3.1.3 form not saving to model

I am following this tutorial
I have gone back and written the code to match exactly. I have another form that works called category_add which is exactly the same as this form. But for the life of me I cannot figure out why bookmark_add doesn't update the database with the form entries.
Models.py
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
title = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
created_by = models.ForeignKey(User, related_name='categories', on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name_plural = 'Categories'
def __str__(self):
return self.title
class Bookmark(models.Model):
category = models.ForeignKey(Category, related_name='bookmarks', on_delete=models.CASCADE)
title = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
url = models.CharField(max_length=255, blank=True)
created_by = models.ForeignKey(User, related_name='bookmarks', on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
View.py
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .forms import BookmarkForm
#login_required
def bookmark_add(request, category_id):
if request.method == 'POST':
form = BookmarkForm(request.POST)
if form.is_valid():
bookmark = form.save(commit=False)
bookmark.created_by = request.user
bookmark.category_id = category_id
bookmark.save()
return redirect('category', category_id=category_id)
else:
form = BookmarkForm()
context = {
'form': form
}
return render(request, 'bookmark/bookmark_add.html', context)
Forms.py
from django.forms import ModelForm
from .models import Bookmark
class BookmarkForm(ModelForm):
class Meta:
model = Bookmark
fields = ['title', 'description', 'url']
Urls.py
path('', dashboard, name='dashboard'),
path('categories/', categories, name='categories'),
path('categories/add/', category_add, name='category_add'),
path('categories/<int:category_id>/', category, name='category'),
path('categories/<int:category_id>/add_bookmark', bookmark_add, name='bookmark_add')
]
bookmark_add.html
{% extends 'core/base.html' %}
{% block content %}
<div class="container">
<h1 class="title">Add link</h1>
<form method="post" action=".">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="button is-primary">Submit</button>
</form>
</div>
{% endblock %}
Solved this!
This was a dumb issue and an oversight on my end. Thanks to the content creator on youtube. I just needed to append "/" to the url path for add_bookmark.
Problem:
path('categories/<int:category_id>/add_bookmark', bookmark_add, name='bookmark_add')
The Fix:
path('categories/<int:category_id>/add_bookmark/', bookmark_add, name='bookmark_add')

How to send multiple model from a classbased view in django

I made a booklist where cover image can be uploaded inside Booklist class. For more image I added another class called Bookcover. Now in Views.py how can I send both Booklist and Bookcover's by using BookListView
models.py file is below
from django.db import models
from django.utils import timezone
class Booklist(models.Model):
title = models.CharField(max_length=100)
author = models.CharField(max_length = 100)
cover = models.ImageField(null=True, blank=True, default='default-book.jpg')
description = models.TextField()
date_posted = models.DateTimeField(default=timezone.now)
price = models.DecimalField(decimal_places=3, max_digits=100)
def __str__(self):
return self.title
class Bookcover(models.Model):
post = models.ForeignKey(Booklist, default=None, on_delete=models.CASCADE)
covers = models.ImageField(upload_to = 'images/')
def __str__(self):
return self.post.title
here is views.py file
from django.shortcuts import render
from django.views.generic import ListView
from .models import Booklist, Bookcover
def home(request):
return render(request, template_name='home/index.html')
class BookListView(ListView):
model = Booklist
template_name = 'home/index.html'
context_object_name = 'books'
ordering = ['-date_posted']
If you make a ForeignKey, Django automatically will generate a relation in reverse to access - in this case - the related BookCovers for a specific Book. Since you did not specify the related_name=… parameter [Django-doc], the name of this relation is modelname_set, so in this case, bookcover_set.
In the template you can access the book covers of a book with:
{% for book in books %}
{{ book.title }}
{% for cover in book.bookcover_set.all %}
<img src="{{ cover.covers.url }}">
{% endfor %}
{% endfor %}
This will result in an N+1 problem however. You can avoid that by using .prefetch_related(…) [Django-doc]:
class BookListView(ListView):
queryset = Booklist.objects.prefetch_related('bookcover_set')
template_name = 'home/index.html'
context_object_name = 'books'
ordering = ['-date_posted']

Categories