How to split django apps if shared view - python

There is a common case I encounter, where I can't find a way to split apps.
The case is when a info of two models is related and needs to be in the same template
An example speaks 1000 words: (2 models - pages + comments).
# models.py
class Page(models.Model):
title = models.CharField()
content = models.TextField()
class Comment(models.Model):
page = models.ForeignKey('Page')
content = models.TextField()
# url.py
...
url(r'^page/(?P<page_pk>\d+)/$', views.ViewPage, name='page-view-no-comments'),
url(r'^comment/(?P<comment_pk>\d+)/$', views.ViewComment, name='comment-view'),
url(r'^page-with-comments/(?P<page_pk>\d+)/$', views.ViewPageWithComments, name='page-view-with-comments'),
...
# views.py
def ViewPage(request, page_pk):
page = get_object_or_404(Page, pk=page_pk)
return render(request, 'view_page.html', {'page':page,})
def ViewComment(request, comment_pk):
comment = get_object_or_404(Comment, pk=comment_pk)
return render(request, 'view_comment.html', {'comment':comment})
def ViewPageWithComments(request, page_pk):
page = get_object_or_404(Page, pk=page_pk)
page_comments = Comment.objects.filter(page=page)
return render(request, 'view_page.html', {'page':page,'page_comments':page_comments'})
In this situation, splitting to Page app and Comment app is problematic, because they share a view (ViewPageWithComments) and url.
My options are:
1) Create an Ajax call to comments, which has crawling problems although Google might have fixed it lately.
2) Create a method of page that calls a method in the comments app that returns html with the comments content. If the method needs more arguments I also need to write a custom filter tag.
3) Decide not to split...
Am I missing something and there's another option? When would you prefer (1) vs (2) ?
Note - I created a very simple example to keep the problem general.

You don't need to split anything, you have the pages, and comments have a foreign key to that so you can just iterate over the pages comments
{% for page in pages %}
{% for comment in page.comment_set.all %}
{% endfor}
{% endfor %}
If you want to be able to use the same template for a version of this page without comments you can just wrap the comment for loop in an {% if show_comments %} statement

Related

Slicing paginator.page_range inside a template

I have page_obj in a template which was returned from a ListView view. Now, I wanted to create links to several pages before and after the current page. Therefore, I wanted to slice page_obj.paginator.page_range this way: page_obj.paginator.page_range[page_obj.number-3:page_obj.number+4]. This works in django shell but for some reason when I did it a template, there is a Template Syntax Error, Could not parse the remainder: '[page_obj.number-3:page_obj.number+4]' from 'page_obj.paginator.page_range[page_obj.number-3:page_obj.number+4]'. Is there a workaround for this case?
P.S. I know I can do it using the whole page_obj.paginator.page_range and then using if statements to check if a page is in the required range, but I thought it's a bit inefficient.
As stated in my comment Django Template Language does not include normal python syntax. The reason for this is Django aims to separate the logic and design of the website. If there is need to perform somewhat complicated logic you either need to use template tags or filters.
For your need either an inclusion tag would work or a simple filter that would take the page_range and return a sliced version of it. A template filter here would not be very useful considering we can only pass one argument to it, meaning it would not be very customizable. Let's assume that your pagination would look very similar or perhaps you would pass the template you use to the tag.
Firstly you need to create a templatetags sub-package in your app and then in that you would add files (e.g. pagination_tags.py) which would contain your tags. The layout would be something like:
your_app/
__init__.py
models.py
templatetags/
__init__.py
pagination_tags.py
views.py
Now in your file pagination_tags.py you want to write your tags. As a reference you may read the howto on Custom template tags and filters in the documentation.
Firstly we declare register which is an instance of template.Library. After which we would write our template tags / filters. We will use an inclusion_tag:
from django import template
register = template.Library()
#register.inclusion_tag('pagination_tag.html')
def show_pagination(page_obj, **kwargs):
left = kwargs.get('left', 3)
right = kwargs.get('right', 4)
pages_iter = page_obj.paginator.page_range[page_obj.number - left:page_obj.number + right]
template = kwargs.get('template', 'default_pagination_template.html')
return {**kwargs, 'page_obj': page_obj, 'pages_iter': pages_iter, 'template': template}
Now we will have a simple template named pagination_tag.html that will simply extend the template name either passed as a keyword argument or default_pagination_template.html:
{% extends template %}
Now in default_pagination_template.html or any other template we can use all the variables in the dictionary that our function show_pagination returns:
{% for page_num in pages_iter %}
Display page links here, etc.
{% endfor %}
You can modify this implementation as per your needs. I will also leave the design and implementation of default_pagination_template.html upto you.
Now in your template where you want to use this, first we will load these tags. Then we will use them:
{% load pagination_tags %}
...
{% show_pagination page_obj left=5 right=6 template="some_custom_template.html" %}

How do I count hits of each element in a list in Django?

So I have a page where multiple articles are listed. (To be precise, TITLES that are outlinked to the articles written on Notion template.) And I want to have a filed in my model that counts the number of clicks of each article. (I don't want to use django-hitcount library).
Let me first show you my code. models.py
class Article(models.Model):
number = models.IntegerField(primary_key=True)
title = models.CharField(max_length=20, default="")
url = models.URLField(max_length=100, default="")
hits = models.IntegerField(default=0)
template
...
<div class="col text-center">
{% for q in allArticles %}
<h2 id={{q.number}}>{{q.title}}</h2>
{% endfor %}
</div>
...
I was thinking of using onclick() event in JavaScript, but then passing data from JavaScript to Django seemed too challenging to me at the moment.
I'd very much appreciate your help. Thanks.
Well, when you dont take up new challenges you stop learning !
The onclick method looks like the best imo, lets see what others suggest.
honestly, using JS and AJAX to communicate with your django server might be dauting at first but it is quite easy really.
if you know how to create a function in your views.py and know a bit of JS, it's just like any other classic functionnality.
Set up your urls.py for the view function that will add a click to the counter:
path('ajax/add_click', views.add_click name="add_click"),
Then, create your view function (pseudo code):
def add_click(request):
# retrieve the article
article_id = request.GET.get("articleId", None)
# then retrieve the object in database, add 1 to the counter save and return a response
Now the "complicated" part, the ajax request:
function add_one_click(articleId) {
$.ajax({
type: "GET",
url: '/ajax/add_click', // you also can use {% url "app_name:add_click" %}
data: {
'articleId': articleId,
},
success: function() {
console.log("hit added to article");
}
});
}
You need to add JS and Ajax lib to your html template for it to works.
Also you need to pass in the onclick attribute the name of the function + the id of the article
onclick="add_one_click({{article.id}})"
One more thing, this type of view, if not protected can lead to get false results.
Instead of having q.url have a new URL(/article_count?id=q.id) which you will define on your Django project
def article_count(req):
_id = req.GET.get('id', '')
# Query Aritcle and get object
q = Article.objects.get(id=_id)
# update the fields for clicks
q.hits += 1
q.save()
# redirect the page
return redirect(q.url)
Edit:
Create a new url that would handle your article click, lets say-
path('article/clicked/<article_number>', views.click_handler, name='click_counter')
Now, in your template use this url for all the article
<div class="col text-center">
{% for q in allArticles %}
<h2 id={{q.number}}>{{q.title}}</h2>
{% endfor %}
</div>
and in your views.py create a new controller
def click_handler(request, article_number):
article = Article.objects.get(number=article_number)
article.hits += 1
article.save()
# now redirect user to the outer link
return redirect(article.url)

Django pass render_to_response template in other template

this is probably a question for absolute beginners since i'm fairly new to progrmaming. I've searched for couple of hours for an adequate solution, i don't know what else to do.
Following problem. I want to have a view that displays. e.g. the 5 latest entries & 5 newest to my database (just an example)
#views.py
import core.models as coremodels
class LandingView(TemplateView):
template_name = "base/index.html"
def index_filtered(request):
last_ones = coremodels.Startup.objects.all().order_by('-id')[:5]
first_ones = coremodels.Startup.objects.all().order_by('id')[:5]
return render_to_response("base/index.html",
{'last_ones': last_ones, 'first_ones' : first_ones})
Index.html shows the HTML content but not the content of the loop
#index.html
<div class="col-md-6">
<p> Chosen Items negative:</p>
{% for startup in last_ones %}
<li><p>{{ startup.title }}</p></li>
{% endfor %}
</div>
<div class="col-md-6">
<p> Chosen Items positive:</p>
{% for startup in first_ones %}
<li><p>{{ startup.title }}</p></li>
{% endfor %}
Here my problem:
How can I get the for loop to render the specific content?
I think Django show render_to_response in template comes very close to my problem, but i don't see a valid solution there.
Thank you for your help.
Chris
--
I edited my code and problem description based on the solutions provided in this thread
the call render_to_response("base/showlatest.html"... renders base/showlatest.html, not index.html.
The view responsible for rendering index.html should pass all data (last_ones and first_ones) to it.
Once you have included the template into index.html
{% include /base/showlatest.html %}
Change the view above (or create a new one or modify the existing, changing urls.py accordingly) to pass the data to it
return render_to_response("index.html",
{'last_ones': last_ones, 'first_ones' : first_ones})
The concept is that the view renders a certain template (index.html), which becomes the html page returned to the client browser.
That one is the template that should receive a certain context (data), so that it can include other reusable pieces (e.g. showlatest.html) and render them correctly.
The include command just copies the content of the specified template (showlatest.html) within the present one (index.html), as if it were typed in and part of it.
So you need to call render_to_response and pass it your data (last_ones and first_ones) in every view that is responsible for rendering a template that includes showlatest.html
Sorry for the twisted wording, some things are easier done than explained.
:)
UPDATE
Your last edit clarified you are using CBV's (Class Based Views).
Then your view should be something along the line:
class LandingView(TemplateView):
template_name = "base/index.html"
def get_context_data(self, **kwargs):
context = super(LandingView, self).get_context_data(**kwargs)
context['last_ones'] = coremodels.Startup.objects.all().order_by('-id')[:5]
context['first_ones'] = coremodels.Startup.objects.all().order_by('id')[:5]
return context
Note: personally I would avoid relying on the id set by the DB to order the records.
Instead, if you can alter the model, add a field to mark when it was created. For example
class Startup(models.Model):
...
created_on = models.DateTimeField(auto_now_add=True, editable=False)
then in your view the query can become
def get_context_data(self, **kwargs):
context = super(LandingView, self).get_context_data(**kwargs)
qs = coremodels.Startup.objects.all().order_by('created_on')
context['first_ones'] = qs[:5]
context['last_ones'] = qs[-5:]
return context

Best way to use get_absolute_url by foreign key field

What is the best way to create get_absolute_url function for Comment if I need url like this: "/article_url#comment-id"?
My models:
class Article(models.Model):
url = models.CharField()
def get_absolute_url(self):
return reverse('article-detail', args=[self.url])
class Comment(models.Model):
related_article = models.ForeignKey(Article)
My variants:
1) very simple, but in this case django will fetch all fields of article, it's terrible for views like "latest 10 comments" on main page:
u = self.related_article.get_absolute_url()
return ''.join([u, '#comment-', str(self.pk)])
2) in this case, function will be independent of Article class changes, but django will fetch pk in first query and url in second query:
u = Article.objects.filter(pk=self.related_article_id) \
.only('pk') \
.get() \
.get_absolute_url()
return ''.join([u, '#comment-', str(self.pk)])
3) in this case, field 'url' was hardcoded, but django will fetch pk and url in one query:
u = Article.objects.filter(pk=self.related_article_id) \
.only('url') \
.get() \
.get_absolute_url()
return ''.join([u, '#comment-', str(self.pk)])
I am not sure if you are gaining much by storing the url in your Article model and providing a get_absolute_url, however, you do not need to worry about trying to explicitly support anchor tags in your URL; just make sure that you are setting the id properly in your tags in the template.
# views.py
def article_detail(request, article_id):
# Proper exception try/except handling excluded:
article = Article.objects.get(id=article_id)
article_comments = Comment.objects.filter(related_article_id=article_id)
return render_to_response('article_detail.html', {'article': article,
'article_comments': article_comments}
# article_detail.html
{% for article_comment in article_comments %}
<div id='comment-{{article_comment.id}}'>
{# Comment information here. #}
</div>
{% endfor %}
If you are interested in hyperlinking a summary list of comments back to the original article where the comment appeared, then you are probably best defining a get_absolute_url function for the Comment model as well.
Edit: Let's say you are showing the 10 latest comments on your main page, where the actual article is not appearing:
# views.py
def main(request):
# Assumes timestamp exists for comments.
# Note: This query will get EXPENSIVE as the number of comments increases,
# and should be replaced with a better design for a production environment.
latest_article_comments = Comment.objects.all().order_by('creation_date')[:10]
render_to_response('main.html', {'latest_article_comments': latest_article_comments})
# main.html
{% for article_comment in article_comments %}
{{article_comment.user_comment}}
{% endfor %}

Pagination of Date-Based Generic Views in Django

I have a pretty simple question. I want to make some date-based generic views on a Django site, but I also want to paginate them. According to the documentation the object_list view has page and paginate_by arguments, but the archive_month view does not. What's the "right" way to do it?
I created a template tag to do template-based pagination on collections passed to the templates that aren't already paginated. Copy the following code to an app/templatetags/pagify.py file.
from django.template import Library, Node, Variable
from django.core.paginator import Paginator
import settings
register = Library()
class PagifyNode(Node):
def __init__(self, items, page_size, varname):
self.items = Variable(items)
self.page_size = int(page_size)
self.varname = varname
def render(self, context):
pages = Paginator(self.items.resolve(context), self.page_size)
request = context['request']
page_num = int(request.GET.get('page', 1))
context[self.varname] = pages.page(page_num)
return ''
#register.tag
def pagify(parser, token):
"""
Usage:
{% pagify items by page_size as varname %}
"""
bits = token.contents.split()
if len(bits) != 6:
raise TemplateSyntaxError, 'pagify tag takes exactly 5 arguments'
if bits[2] != 'by':
raise TemplateSyntaxError, 'second argument to pagify tag must be "by"'
if bits[4] != 'as':
raise TemplateSyntaxError, 'fourth argument to pagify tag must be "as"'
return PagifyNode(bits[1], bits[3], bits[5])
To use it in the templates (assume we've passed in an un-paginated list called items):
{% load pagify %}
{% pagify items by 20 as page %}
{% for item in page %}
{{ item }}
{% endfor %}
The page_size argument (the 20) can be a variable as well. The tag automatically detects page=5 variables in the querystring. And if you ever need to get at the paginator that belong to the page (for a page count, for example), you can simply call:
{{ page.paginator.num_pages }}
Date based generic views don't have pagination. It seems you can't add pagination via wrapping them as well since they return rendered result.
I would simply write my own view in this case. You can check out generic views' code as well, but most of it will probably be unneeded in your case.
Since your question is a valid one, and looking at the code; I wonder why they didn't decouple queryset generation as separate functions. You could just use them and render as you wish then.
I was working on a problem similar to this yesterday, and I found the best solution for me personally was to use the object_list generic view for all date-based pages, but pass a filtered queryset, as follows:
import datetime, time
def post_archive_month(request, year, month, page=0, template_name='post_archive_month.html', **kwargs):
# Convert date to numeric format
date = datetime.date(*time.strptime('%s-%s' % (year, month), '%Y-%b')[:3])
return list_detail.object_list(
request,
queryset = Post.objects.filter(publish__year=date.year, publish__date.month).order_by('-publish',),
paginate_by = 5,
page = page,
template_name = template_name,
**kwargs)
Where the urls.py reads something like:
url(r'^blog/(?P<year>\d{4})/(?P<month>\w{3})/$',
view=path.to.generic_view,
name='archive_month'),
I found this the easiest way around the problem without resorting to hacking the other generic views or writing a custom view.
There is also excellent django-pagination add-on, which is completely independent of underlying view.
Django date-based generic views do not support pagination. There is an open ticket from 2006 on this. If you want, you can try out the code patches supplied to implement this feature. I am not sure why the patches have not been applied to the codebase yet.

Categories