I've made a Django site, but I've drank the Koolaid and I want to make an IPhone version. After putting much thought into I've come up with two options:
Make a whole other site, like i.xxxx.com. Tie it into the same database using Django's sites framework.
Find some time of middleware that reads the user-agent, and changes the template directories dynamically.
I'd really prefer option #2, however; I have some reservations, mainly because the Django documentation discourages changing settings on the fly. I found a snippet that would do the what I'd like. My main issue is having it as seamless as possible, I'd like it to be automagic and transparent to the user.
Has anyone else come across the same issue? Would anyone care to share about how they've tackled making IPhone versions of Django sites?
Update
I went with a combination of middleware and tweaking the template call.
For the middleware, I used minidetector. I like it because it detects a plethora of mobile user-agents. All I have to do is check request.mobile in my views.
For the template call tweak:
def check_mobile(request, template_name):
if request.mobile:
return 'mobile-%s'%template_name
return template_name
I use this for any view that I know I have both versions.
TODO:
Figure out how to access request.mobile in an extended version of render_to_response so I don't have to use check_mobile('template_name.html')
Using the previous automagically fallback to the regular template if no mobile version exists.
Rather than changing the template directories dynamically you could modify the request and add a value that lets your view know if the user is on an iphone or not. Then wrap render_to_response (or whatever you are using for creating HttpResponse objects) to grab the iphone version of the template instead of the standard html version if they are using an iphone.
Detect the user agent in middleware, switch the url bindings, profit!
How? Django request objects have a .urlconf attribute, which can be set by middleware.
From django docs:
Django determines the root URLconf
module to use. Ordinarily, this is the
value of the ROOT_URLCONF setting, but
if the incoming HttpRequest object has
an attribute called urlconf (set by
middleware request processing), its
value will be used in place of the
ROOT_URLCONF setting.
In yourproj/middlware.py, write a class that checks the http_user_agent string:
import re
MOBILE_AGENT_RE=re.compile(r".*(iphone|mobile|androidtouch)",re.IGNORECASE)
class MobileMiddleware(object):
def process_request(self,request):
if MOBILE_AGENT_RE.match(request.META['HTTP_USER_AGENT']):
request.urlconf="yourproj.mobile_urls"
Don't forget to add this to MIDDLEWARE_CLASSES in settings.py:
MIDDLEWARE_CLASSES= [...
'yourproj.middleware.MobileMiddleware',
...]
Create a mobile urlconf, yourproj/mobile_urls.py:
urlpatterns=patterns('',('r'/?$', 'mobile.index'), ...)
I'm developing djangobile, a django mobile extension: http://code.google.com/p/djangobile/
You should take a look at the django-mobileadmin source code, which solved exactly this problem.
Other way would be creating your own template loader that loads templates specific to user agent. This is pretty generic technique and can be use to dynamically determine what template has to be loaded depending on other factors too, like requested language (good companion to existing Django i18n machinery).
Django Book has a section on this subject.
There is a nice article which explains how to render the same data by different templates
http://www.postneo.com/2006/07/26/acknowledging-the-mobile-web-with-django
You still need to automatically redirect the user to mobile site however and this can be done using several methods (your check_mobile trick will work too)
How about redirecting user to i.xxx.com after parsing his UA in some middleware? I highly doubt that mobile users care how url look like, still they can access your site using main url.
best possible scenario: use minidetector to add the extra info to the request, then use django's built in request context to pass it to your templates like so
from django.shortcuts import render_to_response
from django.template import RequestContext
def my_view_on_mobile_and_desktop(request)
.....
render_to_response('regular_template.html',
{'my vars to template':vars},
context_instance=RequestContext(request))
then in your template you are able to introduce stuff like:
<html>
<head>
{% block head %}
<title>blah</title>
{% if request.mobile %}
<link rel="stylesheet" href="{{ MEDIA_URL }}/styles/base-mobile.css">
{% else %}
<link rel="stylesheet" href="{{ MEDIA_URL }}/styles/base-desktop.css">
{% endif %}
</head>
<body>
<div id="navigation">
{% include "_navigation.html" %}
</div>
{% if not request.mobile %}
<div id="sidebar">
<p> sidebar content not fit for mobile </p>
</div>
{% endif %>
<div id="content">
<article>
{% if not request.mobile %}
<aside>
<p> aside content </p>
</aside>
{% endif %}
<p> article content </p>
</aricle>
</div>
</body>
</html>
A simple solution is to create a wrapper around django.shortcuts.render. I put mine in a utils library in the root of my application. The wrapper works by automatically rendering templates in either a "mobile" or "desktop" folder.
In utils.shortcuts:
from django.shortcuts import render
from user_agents import parse
def my_render(request, *args, **kwargs):
"""
An extension of django.shortcuts.render.
Appends 'mobile/' or 'desktop/' to a given template location
to render the appropriate template for mobile or desktop
depends on user_agents python library
https://github.com/selwin/python-user-agents
"""
template_location = args[0]
args_list = list(args)
ua_string = request.META['HTTP_USER_AGENT']
user_agent = parse(ua_string)
if user_agent.is_mobile:
args_list[0] = 'mobile/' + template_location
args = tuple(args_list)
return render(request, *args, **kwargs)
else:
args_list[0] = 'desktop/' + template_location
args = tuple(args_list)
return render(request, *args, **kwargs)
In view:
from utils.shortcuts import my_render
def home(request): return my_render(request, 'home.html')
Related
I am sorry if it question might turn to be little broad, but since I am just learning django (and I am just hobbyist developer) I need some guidance which, I hope, will help someone like me in the future since I could not find any clear and easily comprehensible guide on using this widget. With your answers and help I will try to make this question thread at least guide-ish.
Material I found somewhat helpful for this topic:
Django multi-select widget?
Django: Replacement for the default ManyToMany Widget of Forms
Django's FilteredSelectMultiple widget only works when logged in
Django FilteredSelectMultiple not rendering on page
Use the Django admin app's FilteredSelectMultiple widget in form
Get the chosen values from FilteredSelectMultiple widget in Django
Django FilteredSelectMultiple Right Half Does Not Render
There were few others links, but they did not made anything clearer or added new information so I won't mention them.
Here is what I managed to understand (please correct me if I am wrong or add anything that I missed):
To create FilteredSelectMultiple widget firs I need to amend forms.py (as in any other widget creation process). Amended forms.py should have from django.contrib.admin.widgets import FilteredSelectMultiple import and Media class. forms.py code should look like this (please correct me, because probably it is wrong somewhere):
from django import forms
from catalog.models import DrgCode
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.conf import settings #without it I get an error that settings not defined
class CalculatorForm(forms.Form):
drg_choice = forms.ModelMultipleChoiceField(queryset=DrgCode.objects.all(), widget=FilteredSelectMultiple("Somethings", is_stacked=False), required=True)
class Media:
css = {
'all': (os.path.join(settings.BASE_DIR, '/static/admin/css/widgets.css'),),
}
js = ('/admin/jsi18n',)
Questions about this part:
Am I right about django.conf import? Since I did not see it
imported in any material I found. Answer: during my test I determined that django.conf import is necessary if using settings.BASE_DIR part. In various sources there was two ways of writing css path, this one worked for me.
Do I need to create widgets.css and corresponding directory? Or
django will find it itself? Since there is no such file or directory
generated after I created skeleton-website using django-admin
startproject cmd? Answer: No. There is no need to create widgets.css or any of the files since django finds them itself.
Same question as previous for jsi18n part. Also what is this? I
assume its javascript file, but it has no extension for some reason.
Also I cannot find it anywhere? Should I create it? How to do that?
Or I can copy it from somewhere? Partial answer: no need to create it. Just point at it in urls.py. Still do not know exactly that kind of file it is (or where it is)
After amending forms.py I should ammend urls.pyby adding url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', name='jsi18n')
So urls.py now look like this:
from django.urls import path
from . import views
from django.conf.urls import url
urlpatterns = [
path('', views.index, name='index'),
'django.views.i18n.javascript_catalog',
name='jsi18n'),
]
Questions:
Am I doing it wright or should I just add it below urlpatterns? Answer: This method if fine.
Now I need to set HTML template file for form to render (like in any other case). Code for it (file named DrgCalculator.html):
{% extends "base_generic.html" %}
<script type="text/javascript" src="{% url 'jsi18n' %}" > </script>
{{ form.media }}
<form enctype="multipart/form-data" method="POST">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="save btn btn-default">Submit</button>
</form>
This part seems more or less clear. But maybe I should amend
something or know about? Answer: Should be changed. Will write full code below.
Lastly I need to adjust views.py to set where this form and everything related with it happens.
From what I understand code in this part is more or less is not directly related with widget, but to complete everything and make working example I will use code I leaned/got in this (very good) django tutorial:
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from catalog.models import DrgCode
from catalog.forms import CalculatorForm
def DrgCalculator(request):
if request.method == 'POST':
form = CalculatorForm(request.POST)
if form.is_valid():
return render(request, 'DrgCalculator.html')
context = {
'form': form,
}
return render(request, 'DrgCalculator.html', context)
Questions:
Any remarks on this part of the code? Answer: Missing else: form = DrgCalculator(). Will write amended code below.
How I will access values which user choose using
FilteredSelectMultiple? I imagine I should clean data in
forms.py like with other widgets. So I should add nested function
below to my class CalculatorForm in forms.py am I right? Answer: Yes, data should be cleaned like in other cases. Function is correct.
def clean_CalculatorForm(self):
drg_choice = self.cleaned_data['drg_choice']
return drg_choice
Data I will get after cleaning will be list or dictionary? Am I
right? Answer: No, from this widget user input received as QuerySet
That is all my questions, sorry for long thread, I tried to make it as clear as possible. If I need to clarify something, please let me know. I will try to edit and update this, to make it friendly for people who will read it in future.
EDIT1: Answered some of my questions.
EDIT2: Answered rest of my questions.
After spending few days of research and testing I managed to get FilteredSelectMultiple widget working outside admin page in user form. As promised in question, I will try to synthesize my accumulated knowledge into some sort of guide which, I hope, will help someone like me in the future. Please note that I am far from professional (just a hobbyist with no computer engineering background to be precise) so my observations might not be exactly correct or the way I done it might not the best one. Despite that it might help you to get on the right track.
So to begin with FilteredSelectMultiple widget first of all - forms.py have to be amended by importing widget, creating field for it (similar like with regular widgets) and adding nested Media class. Code example:
from django.contrib.admin.widgets import FilteredSelectMultiple
class DrgCalculator(forms.Form):
drg_choise = forms.ModelMultipleChoiceField(queryset=DrgCode.objects.all(),
label="Something",
widget=FilteredSelectMultiple("Title", is_stacked=False),
required=True)
class Media:
css = {
'all': ('/static/admin/css/widgets.css',),
}
js = ('/admin/jsi18n',)
def clean_drg_choise(self):
drg_choise = self.cleaned_data['drg_choise']
return drg_choise
As I determined during my test and research class Media should be copied as written and require no changes for widget to work. Files mentioned in this class will be found by django itself so no need to search and copy them (like told in some of the material I read).
After creating form urls.py should be amended. During testing I found out, that in newer django versions (or at lest one I used) javascript_catalog is renamed and url provided cannot be string. So code should look like this:
from django.urls import path
from . import views
from django.conf.urls import url
from django import views as django_views
urlpatterns = [
url(r'^jsi18n/$', django_views.i18n.JavaScriptCatalog.as_view(), name='jsi18n'),
]
Now for the htlm template I am sure that there is more ways of doing it so I just provide example:
{% extends "base_generic.html" %}
{% block content %}
<div id='frame'>
<form action="" method="post">
<div id='sk_body'>
<fieldset>
<legend>Fill required fields</legend>
<form>
{% csrf_token %}
<table>
{{ form.media }}
{{ form.as_table }}
<script type="text/javascript" src="{% url 'jsi18n' %}"></script>
</table>
<input type="submit" value="Count">
</form>
</fieldset>
</div>
</form>
</div>
{% endblock %}
To receive data from this widget fairly standard code in views.py should be used, example code I used:
def DRG_calcualtor(request):
if request.method == 'POST':
form = DrgCalculator(request.POST)
if form.is_valid():
choosen_drg = form.cleaned_data['drg_choise'] #result as QuerySet
choosen_drg_list = list([str(i) for i in choosen_drg]) #you can convert it to list or anything you need
return render(request, 'DRGcalculator_valid.html')
context = {
'form': form,
}
return render(request, 'DRGcalculator.html', context)
else:
form = DrgCalculator()
context = {
'form': form,
}
return render(request, 'DRGcalculator.html', context)
I would like to know if anyone has any best practice recommendations for rendering a view within a view in Django, or something with a similar effect where both views can have their own context and methods.
I am currently rendering the dropdown using the built-in include tag. So the nested .html is using the same Django view as the rest of the page. And all the context is coming from the main view as shown below. I would like to give this dropdown its own Django view so that I only perform certain database queries and other calculations if the dropdown is opened.
<div class="container-fluid">
<div class="row">
<div class="col-6">
</div>
<div class="col-2 d-flex justify-content-center">
{% include nested.html %}
</div> ...
It would be nice if someone with experience in this could let me know what they think, and if it even makes sense to try and split this into 2 views.
In summary. I would like to know if I can somehow render a separate view using something similar to the include Django tag. So that I have control over where the view gets rendered.
Below I have outlined my implementation of the solution below, using custom inclusion tags as suggested in the comments to my question.
You can make a custom template tag file in project directory and then register these in the template library. e.g:
template_tags.py
from django import template
from users.models import Organisation
register = template.Library()
#register.inclusion_tag('nested.html', takes_context=True)
def nested_template(context, param=None): #param to pass variables from main view
context.update({
'data_list': get_data_list(param)
'organisation_id': param,
})
return context
def get_data_list(self,param):
data_list = ... #do all queries needed.
return data_list
In the template tag you point to your nested.html which does whatever you need it to do. For example:
nested.html
{% for data in data_list %}
<h4> {{data}} </h4>
{% endfor %}
Then to include the template, in your main view template:
{% load nested_template %} #at the top or anywhere before using it
{% nested_template param %} #where you want the template included
Hopefully clear enough and may assist someone
I'm developing an internal application and I would like to be able to nest my views to keep everything nice and organized. I plan on doing this by keeping different parts of the page in their own HTML files with their own Views (separate sidebar and navbar, separate charts, etc).
views.py
from django.shortcuts import render
from django.views.generic import TemplateView
import Recall.data_logger.models as DLM
class ReportHome(TemplateView):
template_name = 'data_logger/index.html'
class SelectorSidebar(TemplateView):
template_name = 'data_logger/sidebar.html'
def get(self, request, *args, **kwargs):
companies = DLM.Company.objects.order_by('company_name').all()
return render(request, self.template_name, {'companies':companies,})
index.html
<html>
<head></head>
<body data-gr-c-s-loaded="true">
{% include 'data_logger/navbar.html' %}
<div class="container-fluid">
<div class="row">
{% include 'data_logger/sidebar.html' %} <!-- This is the part I need help with-->
</div>
</div>
</body>
</html>
sidebar.html
<div class="col-sm-3 col-md-1 sidebar">
<ul class="nav nav-sidebar">
{% for company in companies %}
<li>{{ company.company_name }}</li>
{% endfor %}
</ul>
</div>
I understand that by just using {% include 'data_logger/sidebar.html' %} it's just loading the HTML and bypassing SelectorSidebar, how do I direct it through the View?
I'd like a solution that allows me to access anything from a simple list of names to relitively large datasets being fed into a D3 chart.
Solution
This is what I ended up using:
index.html
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"
integrity="sha384-vk5WoKIaW/vJyUAd9n/wmopsmNhiy+L2Z+SBxGYnUkunIxVxAv/UtMOhba/xskxh"
crossorigin="anonymous"></script>
<script>
$.get("_sidebar", function(data, status){
$("#_sidebar").html(data);
});
</script>
</head>
<body data-gr-c-s-loaded="true">
{% include 'data_logger/navbar.html' %}
<div class="container-fluid">
<div class="row" id="_sidebar"></div>
</div>
</body>
</html>
Where _sidebar is the URL to SelectorSidebar:
urlpatterns = [
path('', v.ReportHome.as_view(), name='ReportHome'),
path('_sidebar', v.SelectorSidebar.as_view(), name='SelectorSidebar'),
]
I think you are making some confusion on how Django templates and views work together.
In very simple terms a Django template is what defines the HTML code that makes up a page. You can keep your templates very modular and organized; to do this you can use the include template tag or you can use template inheritance, which is a very powerful way to have "modular" templates.
A Django view is basically a function (or a class of you are using class based views) that receive an HTTP request and build an HTTP response.
It doesn't make much sense to have "nested" views because usually you have just one HTTP request and you want to build just a response with the HTML needed to display the page.
So I think that you can happily use Django templates to put together all the modules that make up your page (header, sidebar, etc.), but each page should correspond to a single Django view.
Another approach could use AJAX and Javascript to make different HTTP requests and build up the page client-side, but I think that this is not the approach you are considering here.
As #baxeico answered, you can't have multiple views to serve a page, because one HTTP request is one view.
If you have content that needs to appear on a lot of pages, like your sidebar, and that content also requires some context information to render (like a list of companies to fetch from the db), you have two options:
If the stuff required to add to the sidebar is fairly limited, create a template context processor that you add to the list of context processors in your settings (TEMPLATES setting).
def sidebar_context(request):
return {'companies': DLM.Company.objects.order_by('company_name').all()}
and in your settings, you'd add something like 'myapp.custom_contexts.sidebar_context' at the top of the list.
Now, every single template has access to the context variable companies, including your sidebar template.
If the stuff shown in the sidebar is more dynamic, or more complex, you should consider fetching the data from within the browser using AJAX. You would create a view that returns JSON instead of HTML and in your sidebar template add javascript to fetch the data and populate the sidebar.
The view is as simple as your current one:
def sidebar(request):
return JsonResponse({'companies': Company.objects.all().values('name', 'id')})
which will return a list of dicts containing name and id of each company. In your AJAX handler for the successful response (which receives the data), you can then loop through data and access data[i].name and data[i].id which you can use to populate your list.
I won't go as far as posting the full javascript (please search for jQuery, ajax and django) but here's a bit to give you an idea, assuming jQuery:
$(window).on('load', function() {
$.ajax({
url: "{% url 'sidebar' %}", // assuming this is inside a template, if not {% url %} won't work and you'll have to get it in a different way
success: function(data) {
if (data.length > 0) {
for (var i=0; i<data.length; i++) {
var elem = $("<li>" + data[i].name + "</li>")
$("#companies").append(elem)
}
}
})
})
I have build a web site for a client which has a number of applications. Now he has a new URL registered which he wants to point to the same site, but he wants the look and feel changed. That's basically he wants a new home.html and base.html for the new web site. I can easily add the new site to settings and then change the view for the home page, to display a new home2.html.
However how do I do something like this as expressed in psuedo code in base.html
{% if site_id equals 1 %}
{% include "base1.html" %}
{% endif %}
{% if site_id equals 2 %}
{% include "base2.html" %}
{% endif %}
Any ideas. There are 100s of views on the site and nearly 50 models. I cannot recreate models, and mess around. This needs to be a quick fix.
Thanks in advance
You can create a context processor to automatically add site_id to the context: http://docs.djangoproject.com/en/dev/ref/templates/api/#writing-your-own-context-processors
But I would opt for a different solution. You can simply add an extra template directory per site so Django will try the templates specifically for that site first and fall back to the normal templates if they're not available.
To extend the idea of WoLph with the context processor, I would maybe even add the switching of the template to the context processor which would clean up your templates, as otherwise you may have to repeat the if clause quite often:
from django.contrib.sites.models import Site
def base_template(request):
site = Site.objects.get_current()
template = "base%s.html" % str(site.pk)
return {'BASE_TEMPLATE': template}
And in your template: {% include BASE_TEMPLATE %}
Looks nicer to me than the switching in the templates!
Another solution would be writing a Middleware to set ´request.site´ the current site id.
I'd like everything to function correctly, except when it's mobile, the entire site will used a set of specific templates.
Also, I'd like to autodetect if it's mobile. If so, then use that set of templates throughout the entire site.
Have two sets of templates, one for mobile, one for desktop. Store the filenames in a pair of dictionaries, and use the User-agent header to detect which set should be used. Also allow manual selection of which site to use via a session entry.
If you place a class on your body (Django uses something similar to specify what column style to use), you could use the same templates but simply use different stylesheets. I'm not sure what main differences you are using separate templates for, but this might allow you to cut down on re-coding the templates multiple times.
best practice: use minidetector to add the extra info to the request, then use django's built in request context to pass it to your templates like so.
from django.shortcuts import render_to_response
from django.template import RequestContext
def my_view_on_mobile_and_desktop(request)
.....
render_to_response('regular_template.html',
{'my vars to template':vars},
context_instance=RequestContext(request))
then in your template you are able to introduce stuff like:
<html>
<head>
{% block head %}
<title>blah</title>
{% if request.mobile %}
<link rel="stylesheet" href="{{ MEDIA_URL }}/styles/base-mobile.css">
{% else %}
<link rel="stylesheet" href="{{ MEDIA_URL }}/styles/base-desktop.css">
{% endif %}
</head>
<body>
<div id="navigation">
{% include "_navigation.html" %}
</div>
{% if not request.mobile %}
<div id="sidebar">
<p> sidebar content not fit for mobile </p>
</div>
{% endif %>
<div id="content">
<article>
{% if not request.mobile %}
<aside>
<p> aside content </p>
</aside>
{% endif %}
<p> article content </p>
</aricle>
</div>
</body>
</html>
There are different strategies.
If you've a lot of views that renders to template files for the web version, and don't want to rewrite all views checking if the request is coming from a mobile user-agent, you'd be better writing a Middleware.
A workflow could be like this:
def process request:
if from_mobile:
settings.TEMPLATE_DIRS=settings.TEMPLATE_MOBILE_DIRS
else:
settings.TEMPLATE_DIRS=settings.TEMPLATE_WEB_DIRS
There is only a little problem here: As Django Documentation reports, it's not correct to change settings at runtime: http://docs.djangoproject.com/en/dev/topics/settings/#altering-settings-at-runtime
So you may want to call
django.conf.settings.configure(default_settings, **settings)
The answer depends heavily on the type of your target audience. If you target for modern mobile browsers equivalents to their desktop counterparts (such as WebKit-based), all you need is specific stylesheet with appropriate media query (you are basically designing for low-res rather than mobile).
Totally different strategy is needed if your site (e.g. airline schedules) must to be accessible widest possible range of mobile devices, some of running very old / limited browsers. Then custom (html) templates may be easiest way to go.
You might want to check out mobilesniffer and django-bloom to see if they fit your purposes.