I am creating a forum where users can edit topics. When a user changes a topics title the slug for the topic is changed. When I post the changes to the topic the slug correcting middleware triggers and redirects me to back to the same editing page instead of letting me redirect to the topic itself. It seems django processes the view first and then calls post with the old url causing my changes to be saved but me not ending up in the right place. For example if I hit post without changing anything I am redirected where I should go. How do I fix this?
# middleware.py
class TopicSlugMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if (request.resolver_match and 'topic_slug' in request.resolver_match.kwargs):
try:
topic_slug = request.resolver_match.kwargs['topic_slug']
topic_pk = int(topic_slug.split('-')[0])
topic = Topic.objects.get(pk=topic_pk)
if (topic.slug != topic_slug):
return redirect(reverse(request.resolver_match.url_name, kwargs={'topic_slug': topic.slug}))
except:
raise Http404('The topic you are looking for does not exist')
return response
# views.py
class EditTopicView(View):
template_name = 'forums/edit_topic.html'
def get(self, request, topic_slug):
topic = Topic.objects.get(slug=topic_slug)
if (request.user.profile.role.can_modify_topics or request.user == topic.started_by):
if (request.user.profile.role.can_modify_topics):
form = EditMoveTopicForm(instance=topic)
else:
form = NewTopicForm(instance=topic)
return render(request, self.template_name, {'topic': topic, 'form': form})
else:
raise PermissionDenied('You do not have permission to modify this topic')
def post(self, request, topic_slug):
topic = Topic.objects.get(slug=topic_slug)
if (request.user.profile.role.can_modify_topics or request.user == topic.started_by):
if (request.user.profile.role.can_modify_topics):
form = EditMoveTopicForm(request.POST)
else:
form = NewTopicForm(request.POST)
if (form.is_valid()):
topic.title = form.cleaned_data['title']
topic.tags = form.cleaned_data['tags']
topic.save()
return redirect('/topics/' + topic.slug)
else:
return render(request, self.template_name, {'topic': topic, 'form': form})
else:
raise PermissionDenied('You do not have permission to modify this topic')
Related
I have practically finished the microblogging application and I want to change to class base views now. I read about generic views, but it looks like each of them has specific properties. In my index view I display two models and two forms, so I created IndexView (View) and put the get and post functions there and it works, but it doesn't look like there are any benefits of class views, so I'm probably doing something wrong. Currently, several views repeat a lot of code, and I thought that class views would solve this problem. I would like an example of how such a view should be written as class view with reusable get.
class IndexView(View):
def get(self, request, *args, **kwargs):
post_form = AddPostForm()
comment_form = AddCommentForm()
if request.user.is_authenticated:
logged_user = CustomUser.objects.get(id=request.user.id)
blocked_users = logged_user.blocked.all()
posts = Post.objects.exclude(author__in=blocked_users)
print(blocked_users)
posts = posts.order_by('-pub_date')
else:
posts = Post.objects.all()
posts = posts.order_by('-pub_date')
comments = Comment.objects.all()
comments = comments.order_by("-pub_date")
return render(request, 'mikroblog/index.html', {'posts': posts, 'comments': comments,
'post_form': post_form, 'comment_form': comment_form})
def post(self, request, *args, **kwargs):
if request.method == 'POST':
post_form = AddPostForm(request.POST)
comment_form = AddCommentForm(request.POST)
if post_form.is_valid():
new_post = post_form.save(commit=False)
new_post.author = request.user
new_post.tags = ''
content = ''
for word in new_post.content_post.split():
if '#' in word:
new_post.tags += f"{word} "
content += f"{word} "
else:
content += f"{word} "
new_post.content_post = content
new_post.save()
for word in content.split():
if '#' in word:
print(word)
user_to_notificate = CustomUser.objects.get(username=word[1:])
new_notification = TalkAbout()
new_notification.where = new_post
new_notification._from = new_post.author
new_notification.to = user_to_notificate
new_notification.sended = False
print(new_notification)
new_notification.save()
post_form = AddPostForm()
if comment_form.is_valid():
new_comment = comment_form.save(commit=False)
new_comment.author = request.user
new_comment.save()
comment_form = AddCommentForm()
return HttpResponseRedirect(reverse('index'))
Use class-based views and override the 'post' and 'get' methods if you need to.
Follow this link:
Django CBVs documentation
I have a situation where I should first display a form to the user where the user fill in two fields and after that base on the form fields I query database and show the user a list of objects.
But the problem is I use class based views and I can't access to the cleaned data in my get method. I know that the forms must be handled in post methods not get methods so I can't process form in my get method.
Here is my code:
views.py
class IncomeTransactionReport(LoginRequiredMixin, ListView):
def post(self, request, *args, **kwargs):
# here I get form from post request
form = IncomeReportForm(request.POST)
# if form is valid redirect user to the same page to show the results based on the filled
# in form fields 'from_date' and 'to_date'
if form.is_valid():
from_date = form.cleaned_data['from_date']
to_date = form.cleaned_data['to_date']
return redirect('income_report')
# else render the same page with the form and form errors
else:
error_message = 'Please solve the error and try again'
return render(request, 'report_income.html', context={'error_message': error_message,
'form': form}, status=422)
def get(self, request, *args, **kwargs):
# here I need to access 'from_date' and 'to_date' to query the database and show the results
# in paginated pages to the user
if from_date != None and to_date != None:
incomes = Income.objects.filter(user=user,
date__gte=datetime.date.fromisoformat(from_date),
date__lte=datetime.date.fromisoformat(to_date)).all()
elif from_date != None:
incomes = Income.objects.filter(user=user,
date__gte=datetime.date.fromisoformat(from_date),
date__lte=datetime.date.fromisoformat(from_date) + \
relativedelta.relativedelta(months=+1)).all()
else:
incomes = Income.objects.filter(user=user).all()
page = request.POST.get('page', 1)
paginator = Paginator(incomes, 5)
try:
incomes = paginator.page(page)
except PageNotAnInteger:
incomes = paginator.page(1)
except EmptyPage:
incomes = paginator.page(paginator.num_pages)
message = 'This is your requested list of incomes'
# here I return the results
return render(request, 'report_income.html', {'message': message, 'incomes': incomes})
I you need more information please let me know to post it here.
To anwser your question I'll just describe proper form processing in django. But for the love of god please don't post such questions before reading the documentation that perfectly explayins everything here
This is the example of how to proccess a view that has a form in it:
SomeClassBasedView(TemplateView):
template_name = 'some_template.html'
def get(self, request):
# some custom get processing
form = SomeForm() # inhereting form.Form or models.ModelForm
context = { 'form': form,
# other context
}
return render(request, self.template, context)
def post(self, request):
# this works like 'get data from user and populate form'
form = SomeForm(request.POST)
if form.is_valid():
# now cleaned_data is created by is_valid
# example:
user_age = form.cleaned_data['age']
# some other form proccessing
context = {
'form': SomeForm(),
# other context
}
return render(request, self.template, context)
# if there were errors in form
# we have to display same page with errors
context = {
'form': form,
# other context
}
return render(request, self.template, context)
After a deep search and thinking I found out that the best thing to do is to put form cleaned data in session and access it in other method such as get method.
I used this tutorial to successfully set up a dynamic inline formset using Django. The CreateView works great, but I cannot get the UpdateView to actually update the related fields. There are no errors thrown, but the items will not update. I believe I have isolated the error to the form_valid function. The code is as follows. Thank you for any assistance.
class ApplicantCreate(CreateView):
model = Applicant
success_message = 'Your application was submitted successfully.'
form_class = forms.ApplicantForm
template_name = 'careers/add_applicant.html'
success_url = reverse_lazy('careers:thanks')
def get_context_data(self, **kwargs):
data = super(ApplicantCreate, self).get_context_data(**kwargs)
positions = super(ApplicantCreate, self).get_context_data(**kwargs)
if self.request.POST:
data['employer'] = forms.ApplicantEmployerFormSet(
self.request.POST,
prefix='employer')
data['education'] = forms.ApplicantEducationFormSet(
self.request.POST,
prefix='education')
else:
data['employer'] = forms.ApplicantEmployerFormSet(prefix='employer')
data['education'] = forms.ApplicantEducationFormSet(prefix='education')
return data
context['unfilled_positions'] = Position.objects.filter(filled=False)
return positions
def form_valid(self, form):
context = self.get_context_data()
employer = context['employer']
education = context['education']
with transaction.atomic():
form.instance.created_by = self.request.user
self.object = form.save()
if employer.is_valid():
employer.instance = self.object
employer.save()
if education.is_valid():
education.instance = self.object
education.save()
return super(ApplicantCreate, self).form_valid(form)
def get_success_url(self):
return reverse_lazy('careers:thanks')
class ApplicantUpdate(SuccessMessageMixin,LoginRequiredMixin,GroupRequiredMixin,UpdateView):
group_required = [u'careers-admin',u'careers']
model = Applicant
success_message = '%(first_name)s %(last_name)s was updated successfully.'
form_class = forms.ApplicantUpdateForm
template_name = 'careers/edit_applicant.html'
def get_context_data(self, **kwargs):
data = super(ApplicantUpdate, self).get_context_data(**kwargs)
positions = super(ApplicantUpdate, self).get_context_data(**kwargs)
if self.request.POST:
data['employer'] = forms.ApplicantEmployerFormSet(
self.request.POST,
instance=self.object,
prefix='employer')
data['education'] = forms.ApplicantEducationFormSet(
self.request.POST,
instance=self.object,
prefix='education')
else:
data['employer'] = forms.ApplicantEmployerFormSet(
instance=self.object,
prefix='employer')
data['education'] = forms.ApplicantEducationFormSet(
instance=self.object,
prefix='education')
return data
context['unfilled_positions'] = Position.objects.filter(filled=False)
return positions
def form_valid(self, form):
context = self.get_context_data()
employer = context['employer']
education = context['education']
with transaction.atomic():
form.instance.created_by = self.request.user
self.object = form.save()
if employer.is_valid():
employer.instance = self.object
employer.save()
if education.is_valid():
education.instance = self.object
education.save()
return super(ApplicantUpdate, self).form_valid(form)
def get_success_url(self):
return reverse_lazy('careers:applicant_detail',kwargs={'pk': self.object.pk})
This is not a good blog post, because no errors are shown if any of the inline forms aren't valid. As a user, I wouldn't expect the view to just silently ignore errors in the inline forms, saving my main instance successfully and not reporting back those errors.
Note that I don't know what the errors are, maybe it's just an issue with the management form. But in any case, formset errors should be handled before the main object is actually saved.
In general, if you need to write a view with multiple forms (including a formset), it's better to use a function-based view or a View where you write the get and post than trying to force this into a generic class-based view, which is not meant for this.
def multiple_forms_view(request, object_id):
# if this is a POST request we need to process the form data
obj = get_object_or_404(MyModel, pk=object_id)
if request.method == 'POST':
# create a form instance and populate it with data from the request:
form = MyForm(request.POST, instance=obj)
formset = MyFormSet(request.POST, instance=obj, ...)
# check whether it's valid:
if form.is_valid() and formset.is_valid():
# process the data in form.cleaned_data as required
# ...
# redirect to a new URL:
return HttpResponseRedirect('/thanks/')
# if a GET (or any other method) we'll create a blank form
else:
form = MyForm(instance=obj)
formset = MyFormSet(instance=obj, ...)
return render(request, 'name.html', {'form': form, 'formset': formset})
That way your template can render the errors of each form, including those in the formset. As mentioned before, you could also do this in a View (so your mixins will work), just write the get and post methods and return the same as in the function-based view above.
I have the following class based view that I want to use to render a formset and validate it when it gets submitted through a post method:
The formset renders perfectly. When I submit the form I can read the formset and check it for errors. in the post method of this class -> errors = backorder_formset.errors
If I find any errors in the formset, I would like to render the view, but this time with the formset instance, that I read from POST.
When I call ctx = self.get_context_data() form within the post method of the class the following error gets raised from the call super(MissingProductsListView, self).get_context_data(*args, **kwargs):
'MissingProductsListView' object has no attribute 'object_list'
It seems like the superclass of Listview performs this call:queryset = kwargs.pop('object_list', self.object_list)
My question is why am I running in this error? and how could I render this formset with its errors messages to display it in the template after it was posted? I am using Django 1.9.9
class MissingProductsListView(generic.ListView):
template_name = 'dashboard/purchaseorder/missing_products.html'
context_object_name = 'backorders'
model = BackOrder
def post(self, request, *args, **kwargs):
backorder_formset = BackOrderFormset(request.POST)
errors = backorder_formset.errors
if backorder_formset.is_valid():
# <process form cleaned data>
return HttpResponseRedirect('/success/')
else:
ctx = self.get_context_data()
return self.render_to_response(ctx)
def accumulate_identical_products_from_backorders(self, back_order_list):
... some code
return sorted_accumulated_dict.values()
def get_context_data(self, *args, **kwargs):
ctx = super(MissingProductsListView, self).get_context_data(*args, **kwargs)
ctx['title'] = _("Missing Products")
if self.request.POST:
ctx['back_order_formset'] = BackOrderFormset(self.request.POST)
else:
accumulated_backorders_per_product = self.accumulate_identical_products_from_backorders(BackOrder.objects.all())
back_orders = BackOrderFormset(initial=[{'product_id': backorder_dict['product_id'],
'product': backorder_dict['title'],
'quantity': backorder_dict['quantity']} for backorder_dict in
accumulated_backorders_per_product])
ctx['back_order_formset'] = back_orders
return ctx
def get_queryset(self):
.. some code
return backorder_list
Look here:
class BaseListView(MultipleObjectMixin, View):
"""
A base view for displaying a list of objects.
"""
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
allow_empty = self.get_allow_empty()
if not allow_empty:
# When pagination is enabled and object_list is a queryset,
# it's better to do a cheap query than to load the unpaginated
# queryset in memory.
if self.get_paginate_by(self.object_list) is not None and hasattr(self.object_list, 'exists'):
is_empty = not self.object_list.exists()
else:
is_empty = len(self.object_list) == 0
if is_empty:
raise Http404(_("Empty list and '%(class_name)s.allow_empty' is False.") % {
'class_name': self.__class__.__name__,
})
context = self.get_context_data()
return self.render_to_response(context)
Basically - you missed this part in the POST handler:
self.object_list = self.get_queryset()
And to be honest - I am not quite sure if this is a good idea to add post to the generic ListView in django. It looks more like FormView - but I can be wrong here.
The problem is a bit complex.
In fact, I am not trying to re-invent the wheel and since the back-end dev left I am trying my best not to destroy his code.
But, I think this time I will need to change a lot of things. Or maybe the answer is quite simple and my lake of experience play against me.
Basically, I have a list of articles that you can sort by categories.
I sorted those this way in the URLs:
urlpatterns = patterns(
'',
url(r'^$', ArticleListView.as_view(), name='articles-list'),
url(r'^source/(?P<source>[\w\.#+-]+)/$', SourceEntriesView.as_view(), name='articles-source'),
url(r'^date/(?P<daterealization>[\w\.#+-]+)/$', DateEntriesView.as_view(), name='articles-date'),
url(r'^country/(?P<region>[\w\.#+-]+)/$', RegionEntriesView.as_view(), name='articles-region'),
url(r'^global/$', GlobalEntriesView.as_view(), name='articles-global'),
)
and the main URL is mydomain.com/en/press/
So basically, when I try to sort my articles by sources, for example, I have my article by this category displaying. But the pagination still has all the article.
So, if there is only one article in the category, this only article is showing but my "loadMore button" isn't disabled since it's considering that there are more articles behind.
Here are the principals views.py base-class view, first the base:
class BaseArticleListView(ListView):
"""
Base article list view for manage ajax navigation
"""
model = Article
context_object_name = 'article_list'
template_name = 'base_templates/template_press.html'
paginate_by = get_setting('PAGINATION')
def get_load_more_url(self, request, context):
args = request.GET.copy()
page_obj = context.get('page_obj', None)
if not page_obj or not page_obj.has_next():
return ''
args[self.page_kwarg] = page_obj.next_page_number()
return '?{}'.format(args.urlencode())
def render_to_json_response(self, context, **response_kwargs):
if 'current_app' not in context:
context['current_app'] = resolve(self.request.path).namespace
c = RequestContext(self.request, context)
html_items_list = render_to_string(
'base_templates/template_press.html',
context,
context_instance=c)
html_items_list = html_items_list.strip()
json_response = {
'html_items_list': html_items_list,
'load_more_url': self.get_load_more_url(
self.request,
context)
}
return JsonResponse(json_response)
And the generic list entry view :
#Main article display view
class ArticleListView(FormMixin, BaseArticleView, BaseArticleListView, ContextSourcesMixin):
model = Article
view_url_name = 'djangocms_press:articles-list'
form_class = SourcesRegionsFilterForm
def get_form_kwargs(self):
return {
'initial': self.get_initial(),
'prefix': self.get_prefix(),
'data': self.request.GET or None,
'request': self.request,
}
def get(self, request, *args, **kwargs):
"""
Handle the form submissions to filter by Sources and regions
First_object is use for pagination
"""
context = {}
self.object_list = self.get_queryset().order_by("-date_realization")
first_object = 0
if 'article' in self.request.GET:
try:
project_id = int(request.GET['article'])
context['article_render'] = self.object_list.get(pk=project_id)
except (Article.DoesNotExist, ValueError):
pass
form = self.get_form(self.form_class)
if form.is_valid():
if form.cleaned_data['regions']:
self.object_list = self.object_list.filter(
Q(regions__continent=form.cleaned_data['regions']) | Q(global_regions=True)).distinct()
context.update(self.get_context_data(form=form))
context[self.context_object_name] = context['object_list']
source_qs = ArticleSource.objects.active_translations(get_language()).order_by('translations__name')
date_realization_for_articles = Article.objects.values_list('date_realization',
flat=True).distinct()
region_for_articles = Country.objects.exclude(regions_press_article=None).order_by('name')
context['load_more_url'] = self.get_load_more_url(request, context)
context['dates_realization'] = date_realization_for_articles.dates('date_realization', 'month', order="DESC")
context['sources_list'] = source_qs
context['regions_list'] = region_for_articles
return self.render_to_response(context)
def render_to_json_response(self, context, **response_kwargs):
if 'current_app' not in context:
context['current_app'] = resolve(self.request.path).namespace
c = RequestContext(self.request, context)
html_items_list = render_to_string(
'base_templates/template_press.html',
context,
context_instance=c)
html_items_list = html_items_list.strip()
json_response = {
'html_items_list': html_items_list,
'load_more_url': self.get_load_more_url(self.request, context),
}
return JsonResponse(json_response)
def render_to_response(self, context):
if self.request.is_ajax():
response = self.render_to_json_response(context)
else:
response = super(ArticleListView, self).render_to_response(context)
return response
And you can see that the loadmore button is update at this line :
context['load_more_url'] = self.get_load_more_url(request, context)
end FINALLY the class that manage the sources sorted :
class SourceEntriesView(ContextSourcesMixin, BaseArticleView, BaseArticleListView):
context_object_name = 'article_list'
template_name = 'base_templates/template_press.html'
_source = None
view_url_name = 'djangocms_press:articles-source'
def get(self, *args, **kwargs):
# submit object to cms toolbar to get correct language switcher behavior
if hasattr(self.request, 'toolbar'):
self.request.toolbar.set_object(self.source)
return super(SourceEntriesView, self).get(*args, **kwargs)
#property
def source(self):
if not self._source:
try:
source_qs = ArticleSource.objects.active_translations(
get_language(),
slug=self.kwargs['source']
)
#source_qs = source_qs.filter(site=Site.objects.get_current().pk)
self._source = source_qs.latest('pk')
except ArticleSource.DoesNotExist:
raise Http404("ArticleSource does not exist for this site")
return self._source
def get_queryset(self):
qs = super(SourceEntriesView, self).get_queryset()
if 'source' in self.kwargs:
qs = qs.filter(sources__pk=self.source.pk)
return qs
def get_context_data(self, **kwargs):
kwargs['source'] = self.source
context = super(SourceEntriesView, self).get_context_data(**kwargs)
return context
So, this last class is call in ajax - from the URL - when you select a source. But how to update the load more button from here? I am so lost.
After reading documentation about pagination and base-class view, my conclusion would be to delete those line. But I am afraid to break everything and that I won't be able to make it work properly.
So, my question is what would be the best way of updating a pagination for sorting by some categories an object?
Thank you in advance, at least for the time spent on my request.
So, as it was quite complexe (but finally not) I rewrote everything.
And now it's working, back on the good old day (erase / code again).
Thanks to all the persons who spent time on reading my request, even if unfortunately no one could give an answer !