├── mysite ├── mysite │ ├── __init__.py │ ├── urls.py │ ├── wsgi.py │ └── settings.py ├── polls │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── static │ │ └── polls │ │ │ ├── images │ │ │ └── background.jpg │ │ │ └── style.css │ ├── templates │ │ └── polls │ │ │ ├── results.html │ │ │ ├── index.html │ │ │ └── detail.html │ ├── urls.py │ ├── admin.py │ ├── models.py │ ├── views.py │ └── tests.py ├── manage.py └── templates │ └── admin │ └── base_site.html ├── rebuild_docker.sh ├── static └── admin │ ├── img │ ├── icon-no.gif │ ├── nav-bg.gif │ ├── default-bg.gif │ ├── icon-yes.gif │ ├── icon_alert.gif │ ├── icon_clock.gif │ ├── icon_error.gif │ ├── icon-unknown.gif │ ├── icon_addlink.gif │ ├── icon_success.gif │ ├── tooltag-add.png │ ├── changelist-bg.gif │ ├── deleted-overlay.gif │ ├── icon_calendar.gif │ ├── icon_changelink.gif │ ├── icon_deletelink.gif │ ├── icon_searchbox.png │ ├── inline-delete.png │ ├── inline-restore.png │ ├── nav-bg-grabber.gif │ ├── nav-bg-reverse.gif │ ├── nav-bg-selected.gif │ ├── selector-icons.gif │ ├── selector-search.gif │ ├── sorting-icons.gif │ ├── changelist-bg_rtl.gif │ ├── default-bg-reverse.gif │ ├── gis │ │ ├── move_vertex_off.png │ │ └── move_vertex_on.png │ ├── inline-delete-8bit.png │ ├── inline-restore-8bit.png │ ├── inline-splitter-bg.gif │ └── tooltag-arrowright.png │ ├── js │ ├── jquery.init.js │ ├── prepopulate.min.js │ ├── collapse.min.js │ ├── related-widget-wrapper.js │ ├── collapse.js │ ├── LICENSE-JQUERY.txt │ ├── prepopulate.js │ ├── timeparse.js │ ├── actions.min.js │ ├── inlines.min.js │ ├── SelectBox.js │ ├── admin │ │ ├── RelatedObjectLookups.js │ │ └── DateTimeShortcuts.js │ ├── actions.js │ ├── urlify.js │ ├── calendar.js │ ├── core.js │ ├── SelectFilter2.js │ └── inlines.js │ └── css │ ├── dashboard.css │ ├── login.css │ ├── ie.css │ ├── rtl.css │ ├── changelists.css │ ├── forms.css │ ├── widgets.css │ └── base.css ├── gunicorn_conf.py ├── nginx ├── Dockerfile └── nginx.conf ├── Dockerfile ├── LICENSE ├── docker-compose.yml ├── .gitignore ├── requirements.txt └── README.md /mysite/mysite/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rebuild_docker.sh: -------------------------------------------------------------------------------- 1 | docker-compose build 2 | docker-compose up -d 3 | docker-compose ps 4 | -------------------------------------------------------------------------------- /static/admin/img/icon-no.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/icon-no.gif -------------------------------------------------------------------------------- /static/admin/img/nav-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/nav-bg.gif -------------------------------------------------------------------------------- /static/admin/img/default-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/default-bg.gif -------------------------------------------------------------------------------- /static/admin/img/icon-yes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/icon-yes.gif -------------------------------------------------------------------------------- /static/admin/img/icon_alert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/icon_alert.gif -------------------------------------------------------------------------------- /static/admin/img/icon_clock.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/icon_clock.gif -------------------------------------------------------------------------------- /static/admin/img/icon_error.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/icon_error.gif -------------------------------------------------------------------------------- /gunicorn_conf.py: -------------------------------------------------------------------------------- 1 | # Gunicorn configuration file 2 | 3 | bind = '0.0.0.0:8000' 4 | 5 | loglevel = 'info' 6 | errorlog = '-' 7 | accesslog = '-' 8 | -------------------------------------------------------------------------------- /static/admin/img/icon-unknown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/icon-unknown.gif -------------------------------------------------------------------------------- /static/admin/img/icon_addlink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/icon_addlink.gif -------------------------------------------------------------------------------- /static/admin/img/icon_success.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/icon_success.gif -------------------------------------------------------------------------------- /static/admin/img/tooltag-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/tooltag-add.png -------------------------------------------------------------------------------- /static/admin/img/changelist-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/changelist-bg.gif -------------------------------------------------------------------------------- /static/admin/img/deleted-overlay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/deleted-overlay.gif -------------------------------------------------------------------------------- /static/admin/img/icon_calendar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/icon_calendar.gif -------------------------------------------------------------------------------- /static/admin/img/icon_changelink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/icon_changelink.gif -------------------------------------------------------------------------------- /static/admin/img/icon_deletelink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/icon_deletelink.gif -------------------------------------------------------------------------------- /static/admin/img/icon_searchbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/icon_searchbox.png -------------------------------------------------------------------------------- /static/admin/img/inline-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/inline-delete.png -------------------------------------------------------------------------------- /static/admin/img/inline-restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/inline-restore.png -------------------------------------------------------------------------------- /static/admin/img/nav-bg-grabber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/nav-bg-grabber.gif -------------------------------------------------------------------------------- /static/admin/img/nav-bg-reverse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/nav-bg-reverse.gif -------------------------------------------------------------------------------- /static/admin/img/nav-bg-selected.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/nav-bg-selected.gif -------------------------------------------------------------------------------- /static/admin/img/selector-icons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/selector-icons.gif -------------------------------------------------------------------------------- /static/admin/img/selector-search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/selector-search.gif -------------------------------------------------------------------------------- /static/admin/img/sorting-icons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/sorting-icons.gif -------------------------------------------------------------------------------- /static/admin/img/changelist-bg_rtl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/changelist-bg_rtl.gif -------------------------------------------------------------------------------- /static/admin/img/default-bg-reverse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/default-bg-reverse.gif -------------------------------------------------------------------------------- /static/admin/img/gis/move_vertex_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/gis/move_vertex_off.png -------------------------------------------------------------------------------- /static/admin/img/gis/move_vertex_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/gis/move_vertex_on.png -------------------------------------------------------------------------------- /static/admin/img/inline-delete-8bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/inline-delete-8bit.png -------------------------------------------------------------------------------- /static/admin/img/inline-restore-8bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/inline-restore-8bit.png -------------------------------------------------------------------------------- /static/admin/img/inline-splitter-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/inline-splitter-bg.gif -------------------------------------------------------------------------------- /static/admin/img/tooltag-arrowright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/static/admin/img/tooltag-arrowright.png -------------------------------------------------------------------------------- /mysite/polls/static/polls/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrecp/django-tutorial-docker-nginx-postgres/HEAD/mysite/polls/static/polls/images/background.jpg -------------------------------------------------------------------------------- /mysite/polls/static/polls/style.css: -------------------------------------------------------------------------------- 1 | li a { 2 | color: green; 3 | } 4 | 5 | body { 6 | background: white url("images/background.jpg") no-repeat right bottom; 7 | } 8 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | # Set nginx base image 2 | FROM nginx 3 | 4 | # File Author / Maintainer 5 | MAINTAINER Andre Prado 6 | 7 | # Copy custom configuration file from the current directory 8 | COPY nginx.conf /etc/nginx/nginx.conf 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for sample Django application 2 | 3 | FROM python:2.7-onbuild 4 | 5 | ENV DJANGO_CONFIGURATION Docker 6 | 7 | CMD ["gunicorn", "-c", "gunicorn_conf.py", "--chdir", "mysite", "mysite.wsgi:application", "--reload"] 8 | -------------------------------------------------------------------------------- /mysite/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /mysite/polls/templates/polls/results.html: -------------------------------------------------------------------------------- 1 |

{{ question.question_text }}

2 | 3 | 8 | 9 | Vote again? 10 | -------------------------------------------------------------------------------- /mysite/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | 3 | {% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} 4 | 5 | {% block branding %} 6 |

Polls Administration

7 | 8 | {% endblock %} 9 | 10 | {% block nav-global %}{% endblock %} 11 | -------------------------------------------------------------------------------- /static/admin/js/jquery.init.js: -------------------------------------------------------------------------------- 1 | /* Puts the included jQuery into our own namespace using noConflict and passing 2 | * it 'true'. This ensures that the included jQuery doesn't pollute the global 3 | * namespace (i.e. this preserves pre-existing values for both window.$ and 4 | * window.jQuery). 5 | */ 6 | var django = django || {}; 7 | django.jQuery = jQuery.noConflict(true); 8 | -------------------------------------------------------------------------------- /mysite/mysite/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | 4 | urlpatterns = [ 5 | # Examples: 6 | # url(r'^$', 'mysite.views.home', name='home'), 7 | # url(r'^blog/', include('blog.urls')), 8 | 9 | url(r'^polls/', include('polls.urls', namespace="polls")), 10 | url(r'^admin/', include(admin.site.urls)), 11 | ] 12 | -------------------------------------------------------------------------------- /static/admin/js/prepopulate.min.js: -------------------------------------------------------------------------------- 1 | (function(b){b.fn.prepopulate=function(e,g){return this.each(function(){var a=b(this),d=function(){if(!a.data("_changed")){var f=[];b.each(e,function(h,c){c=b(c);c.val().length>0&&f.push(c.val())});a.val(URLify(f.join(" "),g))}};a.data("_changed",false);a.change(function(){a.data("_changed",true)});a.val()||b(e.join(",")).keyup(d).change(d).focus(d)})}})(django.jQuery); 2 | -------------------------------------------------------------------------------- /mysite/polls/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | url(r'^$', views.IndexView.as_view(), name='index'), 7 | url(r'^(?P[0-9]+)/$', views.DetailView.as_view(), name='detail'), 8 | url(r'^(?P[0-9]+)/results/$', views.ResultsView.as_view(), name='results'), 9 | url(r'^(?P[0-9]+)/vote/$', views.vote, name='vote'), 10 | ] 11 | -------------------------------------------------------------------------------- /mysite/polls/templates/polls/index.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | {% if latest_question_list %} 6 | 11 | {% else %} 12 |

No polls are available.

13 | {% endif %} 14 | -------------------------------------------------------------------------------- /mysite/mysite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for mysite project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Andre 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /mysite/polls/templates/polls/detail.html: -------------------------------------------------------------------------------- 1 |

{{ question.question_text }}

2 | 3 | {% if error_message %}

{{ error_message }}

{% endif %} 4 | 5 |
6 | {% csrf_token %} 7 | {% for choice in question.choice_set.all %} 8 | 9 |
10 | {% endfor %} 11 | 12 |
13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Docker-compose.yml file for atbaker/docker-django project 2 | 3 | # Nginx 4 | nginx: 5 | build: ./nginx 6 | volumes: 7 | - .:/usr/src/app 8 | links: 9 | - django 10 | ports: 11 | - "80:80" 12 | 13 | # This defines a service for the Django app 14 | django: 15 | build: . 16 | volumes: 17 | - .:/usr/src/app 18 | expose: 19 | - "8000" 20 | links: 21 | - postgres 22 | 23 | 24 | # This defines a service for the Postgres database 25 | postgres: 26 | image: postgres:9.3 27 | 28 | 29 | -------------------------------------------------------------------------------- /static/admin/css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* DASHBOARD */ 2 | 3 | .dashboard .module table th { 4 | width: 100%; 5 | } 6 | 7 | .dashboard .module table td { 8 | white-space: nowrap; 9 | } 10 | 11 | .dashboard .module table td a { 12 | display: block; 13 | padding-right: .6em; 14 | } 15 | 16 | /* RECENT ACTIONS MODULE */ 17 | 18 | .module ul.actionlist { 19 | margin-left: 0; 20 | } 21 | 22 | ul.actionlist li { 23 | list-style-type: none; 24 | } 25 | 26 | ul.actionlist li { 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | -o-text-overflow: ellipsis; 30 | } 31 | -------------------------------------------------------------------------------- /static/admin/js/collapse.min.js: -------------------------------------------------------------------------------- 1 | (function(a){a(document).ready(function(){a("fieldset.collapse").each(function(c,b){a(b).find("div.errors").length==0&&a(b).addClass("collapsed").find("h2").first().append(' ('+gettext("Show")+")")});a("fieldset.collapse a.collapse-toggle").click(function(){a(this).closest("fieldset").hasClass("collapsed")?a(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset",[a(this).attr("id")]):a(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset", 2 | [a(this).attr("id")]);return false})})})(django.jQuery); 3 | -------------------------------------------------------------------------------- /mysite/polls/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import Choice, Question 5 | 6 | 7 | class ChoiceInline(admin.TabularInline): 8 | model = Choice 9 | extra = 3 10 | 11 | 12 | class QuestionAdmin(admin.ModelAdmin): 13 | fieldsets = [ 14 | (None, {'fields': ['question_text']}), 15 | ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}), 16 | ] 17 | inlines = [ChoiceInline] 18 | list_display = ('question_text', 'pub_date', 'was_published_recently') 19 | list_filter = ['pub_date'] 20 | search_fields = ['question_text'] 21 | 22 | admin.site.register(Question, QuestionAdmin) 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | -------------------------------------------------------------------------------- /mysite/polls/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | import datetime 4 | 5 | 6 | class Question(models.Model): 7 | question_text = models.CharField(max_length=200) 8 | pub_date = models.DateTimeField('date published') 9 | 10 | def __str__(self): 11 | return self.question_text 12 | 13 | def was_published_recently(self): 14 | now = timezone.now() 15 | return now - datetime.timedelta(days=1) <= self.pub_date <= now 16 | 17 | # Admin view attributes. 18 | was_published_recently.admin_order_field = 'pub_date' 19 | was_published_recently.boolean = True 20 | was_published_recently.short_description = 'Published recently?' 21 | 22 | 23 | 24 | class Choice(models.Model): 25 | question = models.ForeignKey(Question) 26 | choice_text = models.CharField(max_length=200) 27 | votes = models.IntegerField(default=0) 28 | 29 | def __str__(self): 30 | return self.choice_text 31 | -------------------------------------------------------------------------------- /static/admin/js/related-widget-wrapper.js: -------------------------------------------------------------------------------- 1 | django.jQuery(function($){ 2 | function updateLinks() { 3 | var $this = $(this); 4 | var siblings = $this.nextAll('.change-related, .delete-related'); 5 | if (!siblings.length) return; 6 | var value = $this.val(); 7 | if (value) { 8 | siblings.each(function(){ 9 | var elm = $(this); 10 | elm.attr('href', elm.attr('data-href-template').replace('__fk__', value)); 11 | }); 12 | } else siblings.removeAttr('href'); 13 | } 14 | var container = $(document); 15 | container.on('change', '.related-widget-wrapper select', updateLinks); 16 | container.find('.related-widget-wrapper select').each(updateLinks); 17 | container.on('click', '.related-widget-wrapper-link', function(event){ 18 | if (this.href) { 19 | showRelatedObjectPopup(this); 20 | } 21 | event.preventDefault(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /static/admin/js/collapse.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | $(document).ready(function() { 3 | // Add anchor tag for Show/Hide link 4 | $("fieldset.collapse").each(function(i, elem) { 5 | // Don't hide if fields in this fieldset have errors 6 | if ($(elem).find("div.errors").length == 0) { 7 | $(elem).addClass("collapsed").find("h2").first().append(' (' + gettext("Show") + 9 | ')'); 10 | } 11 | }); 12 | // Add toggle to anchor tag 13 | $("fieldset.collapse a.collapse-toggle").click(function(ev) { 14 | if ($(this).closest("fieldset").hasClass("collapsed")) { 15 | // Show 16 | $(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset", [$(this).attr("id")]); 17 | } else { 18 | // Hide 19 | $(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset", [$(this).attr("id")]); 20 | } 21 | return false; 22 | }); 23 | }); 24 | })(django.jQuery); 25 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { 4 | 5 | worker_connections 1024; 6 | 7 | } 8 | 9 | http { 10 | 11 | server { 12 | listen 80; 13 | server_name example.org; 14 | 15 | access_log /dev/stdout; 16 | error_log /dev/stdout info; 17 | 18 | location /static { 19 | alias /usr/src/app/static; 20 | } 21 | 22 | location /static/polls { 23 | alias /usr/src/app/mysite/polls/static/polls; 24 | } 25 | 26 | location /static/admin/js/ { 27 | default_type text/javascript; 28 | alias /usr/src/app/static/admin/js/; 29 | } 30 | 31 | location /static/admin/css/ { 32 | default_type text/css; 33 | alias /usr/src/app/static/admin/css/; 34 | } 35 | 36 | location / { 37 | proxy_pass http://django:8000; 38 | proxy_set_header Host $host; 39 | proxy_set_header X-Real-IP $remote_addr; 40 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 41 | proxy_set_header X-Forwarded-Host $server_name; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /static/admin/js/LICENSE-JQUERY.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 John Resig, http://jquery.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is here because many Platforms as a Service look for 2 | # requirements.txt in the root directory of a project. 3 | # pylibmc==1.2.3 4 | # django-heroku-memcacheify==0.5 5 | # -r requirements/production.txt 6 | 7 | # Bleeding edge Django 8 | django==1.8 9 | 10 | # Configuration 11 | django-configurations==0.8 12 | django-secure==1.0 13 | django-cache-url==0.8.0 14 | dj-database-url==0.3.0 15 | 16 | # Forms 17 | django-braces==1.4.0 18 | django-crispy-forms==1.4.0 19 | django-floppyforms==1.2.0 20 | 21 | # Models 22 | django-model-utils==2.2 23 | 24 | # images 25 | Pillow==2.5.3 26 | 27 | # For user registration, either via email or social 28 | # Well-built with regular release cycles! 29 | django-allauth==0.18.0 30 | 31 | # For the persistance stores 32 | psycopg2==2.5.4 33 | 34 | # Unicode slugification 35 | unicode-slugify==0.1.1 36 | django-autoslug==1.7.2 37 | 38 | # Useful things 39 | django-avatar==2.0 40 | 41 | coverage==3.7.1 42 | Sphinx 43 | 44 | # django-debug-toolbar that works with Django 1.5+ 45 | django-debug-toolbar==1.2.1 46 | 47 | python-memcached==1.53 48 | 49 | gunicorn==19.1.1 50 | 51 | 52 | # Your custom requirements go here 53 | -------------------------------------------------------------------------------- /static/admin/css/login.css: -------------------------------------------------------------------------------- 1 | /* LOGIN FORM */ 2 | 3 | body.login { 4 | background: #eee; 5 | } 6 | 7 | .login #container { 8 | background: white; 9 | border: 1px solid #ccc; 10 | width: 28em; 11 | min-width: 300px; 12 | margin-left: auto; 13 | margin-right: auto; 14 | margin-top: 100px; 15 | } 16 | 17 | .login #content-main { 18 | width: 100%; 19 | } 20 | 21 | .login form { 22 | margin-top: 1em; 23 | } 24 | 25 | .login .form-row { 26 | padding: 4px 0; 27 | float: left; 28 | width: 100%; 29 | } 30 | 31 | .login .form-row label { 32 | padding-right: 0.5em; 33 | line-height: 2em; 34 | font-size: 1em; 35 | clear: both; 36 | color: #333; 37 | } 38 | 39 | .login .form-row #id_username, .login .form-row #id_password { 40 | clear: both; 41 | padding: 6px; 42 | width: 100%; 43 | -webkit-box-sizing: border-box; 44 | -moz-box-sizing: border-box; 45 | box-sizing: border-box; 46 | } 47 | 48 | .login span.help { 49 | font-size: 10px; 50 | display: block; 51 | } 52 | 53 | .login .submit-row { 54 | clear: both; 55 | padding: 1em 0 0 9.4em; 56 | } 57 | 58 | .login .password-reset-link { 59 | text-align: center; 60 | } 61 | -------------------------------------------------------------------------------- /mysite/polls/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Choice', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('choice_text', models.CharField(max_length=200)), 18 | ('votes', models.IntegerField(default=0)), 19 | ], 20 | ), 21 | migrations.CreateModel( 22 | name='Question', 23 | fields=[ 24 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 25 | ('question_text', models.CharField(max_length=200)), 26 | ('pub_date', models.DateTimeField(verbose_name=b'date published')), 27 | ], 28 | ), 29 | migrations.AddField( 30 | model_name='choice', 31 | name='question', 32 | field=models.ForeignKey(to='polls.Question'), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /static/admin/css/ie.css: -------------------------------------------------------------------------------- 1 | /* IE 6 & 7 */ 2 | 3 | /* Proper fixed width for dashboard in IE6 */ 4 | 5 | .dashboard #content { 6 | *width: 768px; 7 | } 8 | 9 | .dashboard #content-main { 10 | *width: 535px; 11 | } 12 | 13 | /* IE 6 ONLY */ 14 | 15 | /* Keep header from flowing off the page */ 16 | 17 | #container { 18 | _position: static; 19 | } 20 | 21 | /* Put the right sidebars back on the page */ 22 | 23 | .colMS #content-related { 24 | _margin-right: 0; 25 | _margin-left: 10px; 26 | _position: static; 27 | } 28 | 29 | /* Put the left sidebars back on the page */ 30 | 31 | .colSM #content-related { 32 | _margin-right: 10px; 33 | _margin-left: -115px; 34 | _position: static; 35 | } 36 | 37 | .form-row { 38 | _height: 1%; 39 | } 40 | 41 | /* Fix right margin for changelist filters in IE6 */ 42 | 43 | #changelist-filter ul { 44 | _margin-right: -10px; 45 | } 46 | 47 | /* IE ignores min-height, but treats height as if it were min-height */ 48 | 49 | .change-list .filtered { 50 | _height: 400px; 51 | } 52 | 53 | /* IE doesn't know alpha transparency in PNGs */ 54 | 55 | .inline-deletelink { 56 | background: transparent url(../img/inline-delete-8bit.png) no-repeat; 57 | } 58 | 59 | /* IE7 doesn't support inline-block */ 60 | .change-list ul.toplinks li { 61 | zoom: 1; 62 | *display: inline; 63 | } 64 | -------------------------------------------------------------------------------- /static/admin/js/prepopulate.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | $.fn.prepopulate = function(dependencies, maxLength) { 3 | /* 4 | Depends on urlify.js 5 | Populates a selected field with the values of the dependent fields, 6 | URLifies and shortens the string. 7 | dependencies - array of dependent fields ids 8 | maxLength - maximum length of the URLify'd string 9 | */ 10 | return this.each(function() { 11 | var prepopulatedField = $(this); 12 | 13 | var populate = function () { 14 | // Bail if the field's value has been changed by the user 15 | if (prepopulatedField.data('_changed')) { 16 | return; 17 | } 18 | 19 | var values = []; 20 | $.each(dependencies, function(i, field) { 21 | field = $(field); 22 | if (field.val().length > 0) { 23 | values.push(field.val()); 24 | } 25 | }); 26 | prepopulatedField.val(URLify(values.join(' '), maxLength)); 27 | }; 28 | 29 | prepopulatedField.data('_changed', false); 30 | prepopulatedField.change(function() { 31 | prepopulatedField.data('_changed', true); 32 | }); 33 | 34 | if (!prepopulatedField.val()) { 35 | $(dependencies.join(',')).keyup(populate).change(populate).focus(populate); 36 | } 37 | }); 38 | }; 39 | })(django.jQuery); 40 | -------------------------------------------------------------------------------- /mysite/polls/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import get_object_or_404, render 2 | from django.http import HttpResponseRedirect 3 | from django.core.urlresolvers import reverse 4 | from django.views import generic 5 | from django.utils import timezone 6 | 7 | from .models import Choice, Question 8 | 9 | 10 | class IndexView(generic.ListView): 11 | template_name = 'polls/index.html' 12 | context_object_name = 'latest_question_list' 13 | 14 | def get_queryset(self): 15 | """ 16 | Return the last five published questions (not including those set to be 17 | published in the future). 18 | """ 19 | return Question.objects.filter( 20 | pub_date__lte=timezone.now() 21 | ).order_by('-pub_date')[:5] 22 | 23 | 24 | class DetailView(generic.DetailView): 25 | model = Question 26 | template_name = 'polls/detail.html' 27 | 28 | def get_queryset(self): 29 | """ 30 | Return the last five published questions (not including those set to be 31 | published in the future). 32 | """ 33 | return Question.objects.filter( 34 | pub_date__lte=timezone.now() 35 | ) 36 | 37 | 38 | class ResultsView(generic.DetailView): 39 | model = Question 40 | template_name = 'polls/results.html' 41 | 42 | 43 | def vote(request, question_id): 44 | p = get_object_or_404(Question, pk=question_id) 45 | try: 46 | selected_choice = p.choice_set.get(pk=request.POST['choice']) 47 | except (KeyError, Choice.DoesNotExist): 48 | # Redisplay the question voting form. 49 | return render(request, 'polls/detail.html', { 50 | 'question': p, 51 | 'error_message': "You didn't select a choice.", 52 | }) 53 | else: 54 | selected_choice.votes += 1 55 | selected_choice.save() 56 | # Always return an HttpResponseRedirect after successfully dealing 57 | # with POST data. This prevents data from being posted twice if a 58 | # user hits the Back button. 59 | return HttpResponseRedirect(reverse('polls:results', args=(p.id,))) 60 | -------------------------------------------------------------------------------- /static/admin/js/timeparse.js: -------------------------------------------------------------------------------- 1 | var timeParsePatterns = [ 2 | // 9 3 | { re: /^\d{1,2}$/i, 4 | handler: function(bits) { 5 | if (bits[0].length == 1) { 6 | return '0' + bits[0] + ':00'; 7 | } else { 8 | return bits[0] + ':00'; 9 | } 10 | } 11 | }, 12 | // 13:00 13 | { re: /^\d{2}[:.]\d{2}$/i, 14 | handler: function(bits) { 15 | return bits[0].replace('.', ':'); 16 | } 17 | }, 18 | // 9:00 19 | { re: /^\d[:.]\d{2}$/i, 20 | handler: function(bits) { 21 | return '0' + bits[0].replace('.', ':'); 22 | } 23 | }, 24 | // 3 am / 3 a.m. / 3am 25 | { re: /^(\d+)\s*([ap])(?:.?m.?)?$/i, 26 | handler: function(bits) { 27 | var hour = parseInt(bits[1]); 28 | if (hour == 12) { 29 | hour = 0; 30 | } 31 | if (bits[2].toLowerCase() == 'p') { 32 | if (hour == 12) { 33 | hour = 0; 34 | } 35 | return (hour + 12) + ':00'; 36 | } else { 37 | if (hour < 10) { 38 | return '0' + hour + ':00'; 39 | } else { 40 | return hour + ':00'; 41 | } 42 | } 43 | } 44 | }, 45 | // 3.30 am / 3:15 a.m. / 3.00am 46 | { re: /^(\d+)[.:](\d{2})\s*([ap]).?m.?$/i, 47 | handler: function(bits) { 48 | var hour = parseInt(bits[1]); 49 | var mins = parseInt(bits[2]); 50 | if (mins < 10) { 51 | mins = '0' + mins; 52 | } 53 | if (hour == 12) { 54 | hour = 0; 55 | } 56 | if (bits[3].toLowerCase() == 'p') { 57 | if (hour == 12) { 58 | hour = 0; 59 | } 60 | return (hour + 12) + ':' + mins; 61 | } else { 62 | if (hour < 10) { 63 | return '0' + hour + ':' + mins; 64 | } else { 65 | return hour + ':' + mins; 66 | } 67 | } 68 | } 69 | }, 70 | // noon 71 | { re: /^no/i, 72 | handler: function(bits) { 73 | return '12:00'; 74 | } 75 | }, 76 | // midnight 77 | { re: /^mid/i, 78 | handler: function(bits) { 79 | return '00:00'; 80 | } 81 | } 82 | ]; 83 | 84 | function parseTimeString(s) { 85 | for (var i = 0; i < timeParsePatterns.length; i++) { 86 | var re = timeParsePatterns[i].re; 87 | var handler = timeParsePatterns[i].handler; 88 | var bits = re.exec(s); 89 | if (bits) { 90 | return handler(bits); 91 | } 92 | } 93 | return s; 94 | } 95 | -------------------------------------------------------------------------------- /static/admin/js/actions.min.js: -------------------------------------------------------------------------------- 1 | (function(a){var f;a.fn.actions=function(q){var b=a.extend({},a.fn.actions.defaults,q),g=a(this),e=!1,m=function(c){c?k():l();a(g).prop("checked",c).parent().parent().toggleClass(b.selectedClass,c)},h=function(){var c=a(g).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},!0));a(b.allToggle).prop("checked",function(){var a;c==g.length?(a=!0,k()):(a=!1,n());return a})},k=function(){a(b.acrossClears).hide(); 2 | a(b.acrossQuestions).show();a(b.allContainer).hide()},p=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()},l=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()},n=function(){l();a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)};a(b.counterContainer).show();a(this).filter(":checked").each(function(c){a(this).parent().parent().toggleClass(b.selectedClass); 3 | h();1==a(b.acrossInput).val()&&p()});a(b.allToggle).show().click(function(){m(a(this).prop("checked"));h()});a("a",b.acrossQuestions).click(function(c){c.preventDefault();a(b.acrossInput).val(1);p()});a("a",b.acrossClears).click(function(c){c.preventDefault();a(b.allToggle).prop("checked",!1);n();m(0);h()});f=null;a(g).click(function(c){c||(c=window.event);var d=c.target?c.target:c.srcElement;if(f&&a.data(f)!=a.data(d)&&!0===c.shiftKey){var e=!1;a(f).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass, 4 | d.checked);a(g).each(function(){if(a.data(this)==a.data(f)||a.data(this)==a.data(d))e=e?!1:!0;e&&a(this).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,d.checked);f=d;h()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){e=!0});a('form#changelist-form button[name="index"]').click(function(a){if(e)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))}); 5 | a('form#changelist-form input[name="_save"]').click(function(c){var d=!1;a("select option:selected",b.actionContainer).each(function(){a(this).val()&&(d=!0)});if(d)return e?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")):confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})}; 6 | a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"}})(django.jQuery); 7 | -------------------------------------------------------------------------------- /mysite/mysite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for mysite project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '07s#p@ol!&7$et@@yh=q^r42qr74pjes!sosahjszt#g1yaqyz' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = ( 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'polls', 41 | ) 42 | 43 | MIDDLEWARE_CLASSES = ( 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | 'django.middleware.security.SecurityMiddleware', 52 | 53 | ) 54 | 55 | ROOT_URLCONF = 'mysite.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'mysite.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 82 | 'NAME': 'postgres', 83 | 'USER': 'postgres', 84 | 'HOST': os.environ['POSTGRES_PORT_5432_TCP_ADDR'], 85 | 'PASSWORD': '', 86 | 'PORT': '' 87 | } 88 | } 89 | 90 | 91 | # Internationalization 92 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 93 | 94 | LANGUAGE_CODE = 'en-us' 95 | 96 | TIME_ZONE = 'Europe/London' 97 | 98 | USE_I18N = True 99 | 100 | USE_L10N = True 101 | 102 | USE_TZ = True 103 | 104 | 105 | # Static files (CSS, JavaScript, Images) 106 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 107 | 108 | STATIC_URL = '/static/' 109 | 110 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Django tutorial using Docker, Nginx, Gunicorn and PostgreSQL. 2 | 3 | This is Django tutorial in steroids, I wanted to go through the tutorial using a configuration that is more close to production. 4 | 5 | I am running the django app with gunicorn, using postgres as the database, the django app is behind nginx and everything is running out of Docker containers orchastrated with docker-compose. 6 | 7 | #### Commits by tutorial parts 8 | 9 | I've just finished going throught Django tutorial so the adventure on this repository is over, however have fun picking a start point: 10 | 11 | [Commit at beggining of Part 2](https://github.com/andrecp/django-tutorial-docker-nginx-postgres/commit/24def5c2e962e74fd41132fb8caef5ef5d9a92f5) 12 | 13 | [Commit at beggining of Part 3](https://github.com/andrecp/django-tutorial-docker-nginx-postgres/commit/3a9bb28648f7fe84675fba6e50c36968d5e22cf5) 14 | 15 | [Commit at beggining of Part 4](https://github.com/andrecp/django-tutorial-docker-nginx-postgres/commit/fda3c34191574867cd07b8c5006f7d1ff6188f52) 16 | 17 | [Commit at beggining of Part 5](https://github.com/andrecp/django-tutorial-docker-nginx-postgres/commit/186309a739908cd2b498aad540c95acf41230f93) 18 | 19 | [Commit at beggining of Part 6](https://github.com/andrecp/django-tutorial-docker-nginx-postgres/commit/d8888f803a36ff862fc84faae87b34b726610f06) 20 | 21 | [Branch with reusuable-apps advanced tutorial](https://github.com/andrecp/django-tutorial-docker-nginx-postgres/tree/reusable-apps) 22 | 23 | #### Setup 24 | I am running this on a Macbook air running Yosemite with boot2docker. 25 | 26 | #### Running 27 | 1. git clone this repo 28 | 2. boot2docker up 29 | 3. sh rebuild_docker.sh 30 | 4. run migrations in your Django instance: ```docker-compose run django /bin/sh -c 'cd mysite;python manage.py migrate'``` 31 | 5. access it in your browser! http://192.168.59.103/ for me, run boot2docker ip to know where it is running. 32 | 33 | #### [docker-compose.yml](https://github.com/andrecp/django-tutorial-docker-nginx-postgres/blob/master/docker-compose.yml) 34 | Compose is a tool for defining and running multi-container applications with Docker. With Compose, you define a multi-container application in a single file, then spin your application up in a single command which does everything that needs to be done to get it running. 35 | 36 | https://docs.docker.com/compose/ 37 | 38 | We have three containers: Nginx, Postgres and a Django App. 39 | 40 | * django 41 | 42 | - Built from ./Dockerfile, running gunicorn to serve our django app. 43 | 44 | * postgres 45 | - Just the basic image from DockerRegistry. 46 | 47 | * nginx 48 | - We are building it from nginx/Dockerfile. In our nginx/nginx.conf we are basically proxying django to gunicorn and serving static files which gunicorn doesn't do for us. 49 | 50 | #### rebuild_docker.sh 51 | Created to make life easier: 52 | 53 | 1. Build your docker-compose file. 54 | - docker-compose build 55 | 56 | 2. Runs your containers on detached mode. 57 | - docker-compose up -d 58 | 59 | 3. Check if they are all running. 60 | - docker-compose ps 61 | 62 | #### Running commands inside containers 63 | use ```docker-compose run CONTAINER_NAME command``` for example: 64 | 65 | * Testing polls app 66 | - docker-compose run django /bin/sh -c 'cd mysite;python manage.py test polls' 67 | 68 | * Running ls 69 | - docker-compose run django /bin/sh -c 'ls' 70 | 71 | * Running Python directly 72 | - docker-compose run django python 73 | 74 | #### Checking logs 75 | use ```docker-compose logs CONTAINER_NAME``` 76 | 77 | #### Important 78 | This project first started as me following the awesome introduction from Andrew T. Baker at the PyCon US 2015 [link](http://docker.atbaker.me/) 79 | 80 | 81 | -------------------------------------------------------------------------------- /static/admin/js/inlines.min.js: -------------------------------------------------------------------------------- 1 | (function(a){a.fn.formset=function(g){var b=a.extend({},a.fn.formset.defaults,g),i=a(this);g=i.parent();var m=function(e,k,h){var j=RegExp("("+k+"-(\\d+|__prefix__))");k=k+"-"+h;a(e).prop("for")&&a(e).prop("for",a(e).prop("for").replace(j,k));if(e.id)e.id=e.id.replace(j,k);if(e.name)e.name=e.name.replace(j,k)},l=a("#id_"+b.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),d=parseInt(l.val(),10),c=a("#id_"+b.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off");l=c.val()===""||c.val()-l.val()>0;i.each(function(){a(this).not("."+ 2 | b.emptyCssClass).addClass(b.formCssClass)});if(i.length&&l){var f;if(i.prop("tagName")=="TR"){i=this.eq(-1).children().length;g.append(''+b.addText+"");f=g.find("tr:last a")}else{i.filter(":last").after('");f=i.filter(":last").next().find("a")}f.click(function(e){e.preventDefault();var k=a("#id_"+b.prefix+"-TOTAL_FORMS");e=a("#"+ 3 | b.prefix+"-empty");var h=e.clone(true);h.removeClass(b.emptyCssClass).addClass(b.formCssClass).attr("id",b.prefix+"-"+d);if(h.is("tr"))h.children(":last").append('
'+b.deleteText+"
");else h.is("ul")||h.is("ol")?h.append('
  • '+b.deleteText+"
  • "):h.children(":first").append(''+b.deleteText+""); 4 | h.find("*").each(function(){m(this,b.prefix,k.val())});h.insertBefore(a(e));a(k).val(parseInt(k.val(),10)+1);d+=1;c.val()!==""&&c.val()-k.val()<=0&&f.parent().hide();h.find("a."+b.deleteCssClass).click(function(j){j.preventDefault();j=a(this).parents("."+b.formCssClass);j.remove();d-=1;b.removed&&b.removed(j);j=a("."+b.formCssClass);a("#id_"+b.prefix+"-TOTAL_FORMS").val(j.length);if(c.val()===""||c.val()-j.length>0)f.parent().show();for(var n=0,o=j.length;n b) return 1; 100 | if (a < b) return -1; 101 | } 102 | catch (e) { 103 | // silently fail on IE 'unknown' exception 104 | } 105 | return 0; 106 | } ); 107 | }, 108 | select_all: function(id) { 109 | var box = document.getElementById(id); 110 | for (var i = 0; i < box.options.length; i++) { 111 | box.options[i].selected = 'selected'; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /static/admin/js/admin/RelatedObjectLookups.js: -------------------------------------------------------------------------------- 1 | // Handles related-objects functionality: lookup link for raw_id_fields 2 | // and Add Another links. 3 | 4 | function html_unescape(text) { 5 | // Unescape a string that was escaped using django.utils.html.escape. 6 | text = text.replace(/</g, '<'); 7 | text = text.replace(/>/g, '>'); 8 | text = text.replace(/"/g, '"'); 9 | text = text.replace(/'/g, "'"); 10 | text = text.replace(/&/g, '&'); 11 | return text; 12 | } 13 | 14 | // IE doesn't accept periods or dashes in the window name, but the element IDs 15 | // we use to generate popup window names may contain them, therefore we map them 16 | // to allowed characters in a reversible way so that we can locate the correct 17 | // element when the popup window is dismissed. 18 | function id_to_windowname(text) { 19 | text = text.replace(/\./g, '__dot__'); 20 | text = text.replace(/\-/g, '__dash__'); 21 | return text; 22 | } 23 | 24 | function windowname_to_id(text) { 25 | text = text.replace(/__dot__/g, '.'); 26 | text = text.replace(/__dash__/g, '-'); 27 | return text; 28 | } 29 | 30 | function showAdminPopup(triggeringLink, name_regexp) { 31 | var name = triggeringLink.id.replace(name_regexp, ''); 32 | name = id_to_windowname(name); 33 | var href = triggeringLink.href; 34 | if (href.indexOf('?') == -1) { 35 | href += '?_popup=1'; 36 | } else { 37 | href += '&_popup=1'; 38 | } 39 | var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); 40 | win.focus(); 41 | return false; 42 | } 43 | 44 | function showRelatedObjectLookupPopup(triggeringLink) { 45 | return showAdminPopup(triggeringLink, /^lookup_/); 46 | } 47 | 48 | function dismissRelatedLookupPopup(win, chosenId) { 49 | var name = windowname_to_id(win.name); 50 | var elem = document.getElementById(name); 51 | if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) { 52 | elem.value += ',' + chosenId; 53 | } else { 54 | document.getElementById(name).value = chosenId; 55 | } 56 | win.close(); 57 | } 58 | 59 | function showRelatedObjectPopup(triggeringLink) { 60 | var name = triggeringLink.id.replace(/^(change|add|delete)_/, ''); 61 | name = id_to_windowname(name); 62 | var href = triggeringLink.href; 63 | var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); 64 | win.focus(); 65 | return false; 66 | } 67 | 68 | function dismissAddRelatedObjectPopup(win, newId, newRepr) { 69 | // newId and newRepr are expected to have previously been escaped by 70 | // django.utils.html.escape. 71 | newId = html_unescape(newId); 72 | newRepr = html_unescape(newRepr); 73 | var name = windowname_to_id(win.name); 74 | var elem = document.getElementById(name); 75 | var o; 76 | if (elem) { 77 | var elemName = elem.nodeName.toUpperCase(); 78 | if (elemName == 'SELECT') { 79 | o = new Option(newRepr, newId); 80 | elem.options[elem.options.length] = o; 81 | o.selected = true; 82 | } else if (elemName == 'INPUT') { 83 | if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) { 84 | elem.value += ',' + newId; 85 | } else { 86 | elem.value = newId; 87 | } 88 | } 89 | // Trigger a change event to update related links if required. 90 | django.jQuery(elem).trigger('change'); 91 | } else { 92 | var toId = name + "_to"; 93 | o = new Option(newRepr, newId); 94 | SelectBox.add_to_cache(toId, o); 95 | SelectBox.redisplay(toId); 96 | } 97 | win.close(); 98 | } 99 | 100 | function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) { 101 | objId = html_unescape(objId); 102 | newRepr = html_unescape(newRepr); 103 | var id = windowname_to_id(win.name).replace(/^edit_/, ''); 104 | var selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]); 105 | var selects = django.jQuery(selectsSelector); 106 | selects.find('option').each(function() { 107 | if (this.value == objId) { 108 | this.innerHTML = newRepr; 109 | this.value = newId; 110 | } 111 | }); 112 | win.close(); 113 | }; 114 | 115 | function dismissDeleteRelatedObjectPopup(win, objId) { 116 | objId = html_unescape(objId); 117 | var id = windowname_to_id(win.name).replace(/^delete_/, ''); 118 | var selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]); 119 | var selects = django.jQuery(selectsSelector); 120 | selects.find('option').each(function() { 121 | if (this.value == objId) { 122 | django.jQuery(this).remove(); 123 | } 124 | }).trigger('change'); 125 | win.close(); 126 | }; 127 | 128 | // Kept for backward compatibility 129 | showAddAnotherPopup = showRelatedObjectPopup; 130 | dismissAddAnotherPopup = dismissAddRelatedObjectPopup; 131 | -------------------------------------------------------------------------------- /static/admin/css/rtl.css: -------------------------------------------------------------------------------- 1 | body { 2 | direction: rtl; 3 | } 4 | 5 | /* LOGIN */ 6 | 7 | .login .form-row { 8 | float: right; 9 | } 10 | 11 | .login .form-row label { 12 | float: right; 13 | padding-left: 0.5em; 14 | padding-right: 0; 15 | text-align: left; 16 | } 17 | 18 | .login .submit-row { 19 | clear: both; 20 | padding: 1em 9.4em 0 0; 21 | } 22 | 23 | /* GLOBAL */ 24 | 25 | th { 26 | text-align: right; 27 | } 28 | 29 | .module h2, .module caption { 30 | text-align: right; 31 | } 32 | 33 | .addlink, .changelink { 34 | padding-left: 0px; 35 | padding-right: 12px; 36 | background-position: 100% 0.2em; 37 | } 38 | 39 | .deletelink { 40 | padding-left: 0px; 41 | padding-right: 12px; 42 | background-position: 100% 0.25em; 43 | } 44 | 45 | .object-tools { 46 | float: left; 47 | } 48 | 49 | thead th:first-child, 50 | tfoot td:first-child { 51 | border-left: 1px solid #ddd !important; 52 | } 53 | 54 | /* LAYOUT */ 55 | 56 | #user-tools { 57 | right: auto; 58 | left: 0; 59 | text-align: left; 60 | } 61 | 62 | div.breadcrumbs { 63 | text-align: right; 64 | } 65 | 66 | #content-main { 67 | float: right; 68 | } 69 | 70 | #content-related { 71 | float: left; 72 | margin-left: -19em; 73 | margin-right: auto; 74 | } 75 | 76 | .colMS { 77 | margin-left: 20em !important; 78 | margin-right: 10px !important; 79 | } 80 | 81 | /* SORTABLE TABLES */ 82 | 83 | table thead th.sorted .sortoptions { 84 | float: left; 85 | } 86 | 87 | thead th.sorted .text { 88 | padding-right: 0; 89 | padding-left: 42px; 90 | } 91 | 92 | /* dashboard styles */ 93 | 94 | .dashboard .module table td a { 95 | padding-left: .6em; 96 | padding-right: 12px; 97 | } 98 | 99 | /* changelists styles */ 100 | 101 | .change-list .filtered { 102 | background: white url(../img/changelist-bg_rtl.gif) top left repeat-y !important; 103 | } 104 | 105 | .change-list .filtered table { 106 | border-left: 1px solid #ddd; 107 | border-right: 0px none; 108 | } 109 | 110 | #changelist-filter { 111 | right: auto; 112 | left: 0; 113 | border-left: 0px none; 114 | border-right: 1px solid #ddd; 115 | } 116 | 117 | .change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull { 118 | margin-right: 0px !important; 119 | margin-left: 160px !important; 120 | } 121 | 122 | #changelist-filter li.selected { 123 | border-left: 0px none; 124 | padding-left: 0px; 125 | margin-left: 0; 126 | border-right: 5px solid #ccc; 127 | padding-right: 5px; 128 | margin-right: -10px; 129 | } 130 | 131 | .filtered .actions { 132 | border-left:1px solid #DDDDDD; 133 | margin-left:160px !important; 134 | border-right: 0 none; 135 | margin-right:0 !important; 136 | } 137 | 138 | #changelist table tbody td:first-child, #changelist table tbody th:first-child { 139 | border-right: 0; 140 | border-left: 1px solid #ddd; 141 | } 142 | 143 | /* FORMS */ 144 | 145 | .aligned label { 146 | padding: 0 0 3px 1em; 147 | float: right; 148 | } 149 | 150 | .submit-row { 151 | text-align: left 152 | } 153 | 154 | .submit-row p.deletelink-box { 155 | float: right; 156 | } 157 | 158 | .submit-row .deletelink { 159 | background: url(../img/icon_deletelink.gif) 0 50% no-repeat; 160 | padding-right: 14px; 161 | } 162 | 163 | .vDateField, .vTimeField { 164 | margin-left: 2px; 165 | } 166 | 167 | form ul.inline li { 168 | float: right; 169 | padding-right: 0; 170 | padding-left: 7px; 171 | } 172 | 173 | input[type=submit].default, .submit-row input.default { 174 | float: left; 175 | } 176 | 177 | fieldset .field-box { 178 | float: right; 179 | margin-left: 20px; 180 | margin-right: 0; 181 | } 182 | 183 | .errorlist li { 184 | background-position: 100% .3em; 185 | padding: 4px 25px 4px 5px; 186 | } 187 | 188 | .errornote { 189 | background-position: 100% .3em; 190 | padding: 4px 25px 4px 5px; 191 | } 192 | 193 | /* WIDGETS */ 194 | 195 | .calendarnav-previous { 196 | top: 0; 197 | left: auto; 198 | right: 0; 199 | } 200 | 201 | .calendarnav-next { 202 | top: 0; 203 | right: auto; 204 | left: 0; 205 | } 206 | 207 | .calendar caption, .calendarbox h2 { 208 | text-align: center; 209 | } 210 | 211 | .selector { 212 | float: right; 213 | } 214 | 215 | .selector .selector-filter { 216 | text-align: right; 217 | } 218 | 219 | .inline-deletelink { 220 | float: left; 221 | } 222 | 223 | /* MISC */ 224 | 225 | .inline-related h2, .inline-group h2 { 226 | text-align: right 227 | } 228 | 229 | .inline-related h3 span.delete { 230 | padding-right: 20px; 231 | padding-left: inherit; 232 | left: 10px; 233 | right: inherit; 234 | float:left; 235 | } 236 | 237 | .inline-related h3 span.delete label { 238 | margin-left: inherit; 239 | margin-right: 2px; 240 | } 241 | 242 | /* IE7 specific bug fixes */ 243 | 244 | div.colM { 245 | position: relative; 246 | } 247 | 248 | .submit-row input { 249 | float: left; 250 | } 251 | -------------------------------------------------------------------------------- /static/admin/js/actions.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | var lastChecked; 3 | 4 | $.fn.actions = function(opts) { 5 | var options = $.extend({}, $.fn.actions.defaults, opts); 6 | var actionCheckboxes = $(this); 7 | var list_editable_changed = false; 8 | var checker = function(checked) { 9 | if (checked) { 10 | showQuestion(); 11 | } else { 12 | reset(); 13 | } 14 | $(actionCheckboxes).prop("checked", checked) 15 | .parent().parent().toggleClass(options.selectedClass, checked); 16 | }, 17 | updateCounter = function() { 18 | var sel = $(actionCheckboxes).filter(":checked").length; 19 | // _actions_icnt is defined in the generated HTML 20 | // and contains the total amount of objects in the queryset 21 | $(options.counterContainer).html(interpolate( 22 | ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), { 23 | sel: sel, 24 | cnt: _actions_icnt 25 | }, true)); 26 | $(options.allToggle).prop("checked", function() { 27 | var value; 28 | if (sel == actionCheckboxes.length) { 29 | value = true; 30 | showQuestion(); 31 | } else { 32 | value = false; 33 | clearAcross(); 34 | } 35 | return value; 36 | }); 37 | }, 38 | showQuestion = function() { 39 | $(options.acrossClears).hide(); 40 | $(options.acrossQuestions).show(); 41 | $(options.allContainer).hide(); 42 | }, 43 | showClear = function() { 44 | $(options.acrossClears).show(); 45 | $(options.acrossQuestions).hide(); 46 | $(options.actionContainer).toggleClass(options.selectedClass); 47 | $(options.allContainer).show(); 48 | $(options.counterContainer).hide(); 49 | }, 50 | reset = function() { 51 | $(options.acrossClears).hide(); 52 | $(options.acrossQuestions).hide(); 53 | $(options.allContainer).hide(); 54 | $(options.counterContainer).show(); 55 | }, 56 | clearAcross = function() { 57 | reset(); 58 | $(options.acrossInput).val(0); 59 | $(options.actionContainer).removeClass(options.selectedClass); 60 | }; 61 | // Show counter by default 62 | $(options.counterContainer).show(); 63 | // Check state of checkboxes and reinit state if needed 64 | $(this).filter(":checked").each(function(i) { 65 | $(this).parent().parent().toggleClass(options.selectedClass); 66 | updateCounter(); 67 | if ($(options.acrossInput).val() == 1) { 68 | showClear(); 69 | } 70 | }); 71 | $(options.allToggle).show().click(function() { 72 | checker($(this).prop("checked")); 73 | updateCounter(); 74 | }); 75 | $("a", options.acrossQuestions).click(function(event) { 76 | event.preventDefault(); 77 | $(options.acrossInput).val(1); 78 | showClear(); 79 | }); 80 | $("a", options.acrossClears).click(function(event) { 81 | event.preventDefault(); 82 | $(options.allToggle).prop("checked", false); 83 | clearAcross(); 84 | checker(0); 85 | updateCounter(); 86 | }); 87 | lastChecked = null; 88 | $(actionCheckboxes).click(function(event) { 89 | if (!event) { event = window.event; } 90 | var target = event.target ? event.target : event.srcElement; 91 | if (lastChecked && $.data(lastChecked) != $.data(target) && event.shiftKey === true) { 92 | var inrange = false; 93 | $(lastChecked).prop("checked", target.checked) 94 | .parent().parent().toggleClass(options.selectedClass, target.checked); 95 | $(actionCheckboxes).each(function() { 96 | if ($.data(this) == $.data(lastChecked) || $.data(this) == $.data(target)) { 97 | inrange = (inrange) ? false : true; 98 | } 99 | if (inrange) { 100 | $(this).prop("checked", target.checked) 101 | .parent().parent().toggleClass(options.selectedClass, target.checked); 102 | } 103 | }); 104 | } 105 | $(target).parent().parent().toggleClass(options.selectedClass, target.checked); 106 | lastChecked = target; 107 | updateCounter(); 108 | }); 109 | $('form#changelist-form table#result_list tr').find('td:gt(0) :input').change(function() { 110 | list_editable_changed = true; 111 | }); 112 | $('form#changelist-form button[name="index"]').click(function(event) { 113 | if (list_editable_changed) { 114 | return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost.")); 115 | } 116 | }); 117 | $('form#changelist-form input[name="_save"]').click(function(event) { 118 | var action_changed = false; 119 | $('select option:selected', options.actionContainer).each(function() { 120 | if ($(this).val()) { 121 | action_changed = true; 122 | } 123 | }); 124 | if (action_changed) { 125 | if (list_editable_changed) { 126 | return confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")); 127 | } else { 128 | return confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button.")); 129 | } 130 | } 131 | }); 132 | }; 133 | /* Setup plugin defaults */ 134 | $.fn.actions.defaults = { 135 | actionContainer: "div.actions", 136 | counterContainer: "span.action-counter", 137 | allContainer: "div.actions span.all", 138 | acrossInput: "div.actions input.select-across", 139 | acrossQuestions: "div.actions span.question", 140 | acrossClears: "div.actions span.clear", 141 | allToggle: "#action-toggle", 142 | selectedClass: "selected" 143 | }; 144 | })(django.jQuery); 145 | -------------------------------------------------------------------------------- /mysite/polls/tests.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.utils import timezone 4 | from django.test import TestCase 5 | from django.core.urlresolvers import reverse 6 | 7 | from .models import Question 8 | 9 | 10 | def create_question(question_text, days): 11 | """ 12 | Creates a question with the given `question_text` published the given 13 | number of `days` offset to now (negative for questions published 14 | in the past, positive for questions that have yet to be published). 15 | """ 16 | time = timezone.now() + datetime.timedelta(days=days) 17 | return Question.objects.create(question_text=question_text, 18 | pub_date=time) 19 | 20 | 21 | class QuestionViewTests(TestCase): 22 | def test_index_view_with_no_questions(self): 23 | """ 24 | If no questions exist, an appropriate message should be displayed. 25 | """ 26 | response = self.client.get(reverse('polls:index')) 27 | self.assertEqual(response.status_code, 200) 28 | self.assertContains(response, "No polls are available.") 29 | self.assertQuerysetEqual(response.context['latest_question_list'], []) 30 | 31 | def test_index_view_with_a_past_question(self): 32 | """ 33 | Questions with a pub_date in the past should be displayed on the 34 | index page. 35 | """ 36 | create_question(question_text="Past question.", days=-30) 37 | response = self.client.get(reverse('polls:index')) 38 | self.assertQuerysetEqual( 39 | response.context['latest_question_list'], 40 | [''] 41 | ) 42 | 43 | def test_index_view_with_a_future_question(self): 44 | """ 45 | Questions with a pub_date in the future should not be displayed on 46 | the index page. 47 | """ 48 | create_question(question_text="Future question.", days=30) 49 | response = self.client.get(reverse('polls:index')) 50 | self.assertContains(response, "No polls are available.", 51 | status_code=200) 52 | self.assertQuerysetEqual(response.context['latest_question_list'], []) 53 | 54 | def test_index_view_with_future_question_and_past_question(self): 55 | """ 56 | Even if both past and future questions exist, only past questions 57 | should be displayed. 58 | """ 59 | create_question(question_text="Past question.", days=-30) 60 | create_question(question_text="Future question.", days=30) 61 | response = self.client.get(reverse('polls:index')) 62 | self.assertQuerysetEqual( 63 | response.context['latest_question_list'], 64 | [''] 65 | ) 66 | 67 | def test_index_view_with_two_past_questions(self): 68 | """ 69 | The questions index page may display multiple questions. 70 | """ 71 | create_question(question_text="Past question 1.", days=-30) 72 | create_question(question_text="Past question 2.", days=-5) 73 | response = self.client.get(reverse('polls:index')) 74 | self.assertQuerysetEqual( 75 | response.context['latest_question_list'], 76 | ['', ''] 77 | ) 78 | 79 | 80 | class QuestionIndexDetailTests(TestCase): 81 | def test_detail_view_with_a_future_question(self): 82 | """ 83 | The detail view of a question with a pub_date in the future should 84 | return a 404 not found. 85 | """ 86 | future_question = create_question(question_text='Future question.', 87 | days=5) 88 | response = self.client.get(reverse('polls:detail', 89 | args=(future_question.id,))) 90 | self.assertEqual(response.status_code, 404) 91 | 92 | def test_detail_view_with_a_past_question(self): 93 | """ 94 | The detail view of a question with a pub_date in the past should 95 | display the question's text. 96 | """ 97 | past_question = create_question(question_text='Past Question.', 98 | days=-5) 99 | response = self.client.get(reverse('polls:detail', 100 | args=(past_question.id,))) 101 | self.assertContains(response, past_question.question_text, 102 | status_code=200) 103 | 104 | 105 | class QuestionMethodTests(TestCase): 106 | 107 | def test_was_published_recently_with_future_question(self): 108 | """ 109 | was_published_recently() should return False for questions whose 110 | pub_date is in the future. 111 | """ 112 | time = timezone.now() + datetime.timedelta(days=30) 113 | future_question = Question(pub_date=time) 114 | self.assertEqual(future_question.was_published_recently(), False) 115 | 116 | def test_was_published_recently_with_old_question(self): 117 | """ 118 | was_published_recently() should return False for questions whose 119 | pub_date is older than 1 day. 120 | """ 121 | time = timezone.now() - datetime.timedelta(days=30) 122 | old_question = Question(pub_date=time) 123 | self.assertEqual(old_question.was_published_recently(), False) 124 | 125 | def test_was_published_recently_with_recent_question(self): 126 | """ 127 | was_published_recently() should return True for questions whose 128 | pub_date is within the last day. 129 | """ 130 | time = timezone.now() - datetime.timedelta(hours=1) 131 | recent_question = Question(pub_date=time) 132 | self.assertEqual(recent_question.was_published_recently(), True) 133 | -------------------------------------------------------------------------------- /static/admin/js/urlify.js: -------------------------------------------------------------------------------- 1 | var LATIN_MAP = { 2 | 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', 'Ç': 3 | 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', 'Î': 'I', 4 | 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö': 5 | 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'Ű': 'U', 6 | 'Ý': 'Y', 'Þ': 'TH', 'Ÿ': 'Y', 'ß': 'ss', 'à':'a', 'á':'a', 'â': 'a', 'ã': 7 | 'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', 'è': 'e', 'é': 'e', 'ê': 'e', 8 | 'ë': 'e', 'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 9 | 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 10 | 'ú': 'u', 'û': 'u', 'ü': 'u', 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y' 11 | }; 12 | var LATIN_SYMBOLS_MAP = { 13 | '©':'(c)' 14 | }; 15 | var GREEK_MAP = { 16 | 'α':'a', 'β':'b', 'γ':'g', 'δ':'d', 'ε':'e', 'ζ':'z', 'η':'h', 'θ':'8', 17 | 'ι':'i', 'κ':'k', 'λ':'l', 'μ':'m', 'ν':'n', 'ξ':'3', 'ο':'o', 'π':'p', 18 | 'ρ':'r', 'σ':'s', 'τ':'t', 'υ':'y', 'φ':'f', 'χ':'x', 'ψ':'ps', 'ω':'w', 19 | 'ά':'a', 'έ':'e', 'ί':'i', 'ό':'o', 'ύ':'y', 'ή':'h', 'ώ':'w', 'ς':'s', 20 | 'ϊ':'i', 'ΰ':'y', 'ϋ':'y', 'ΐ':'i', 21 | 'Α':'A', 'Β':'B', 'Γ':'G', 'Δ':'D', 'Ε':'E', 'Ζ':'Z', 'Η':'H', 'Θ':'8', 22 | 'Ι':'I', 'Κ':'K', 'Λ':'L', 'Μ':'M', 'Ν':'N', 'Ξ':'3', 'Ο':'O', 'Π':'P', 23 | 'Ρ':'R', 'Σ':'S', 'Τ':'T', 'Υ':'Y', 'Φ':'F', 'Χ':'X', 'Ψ':'PS', 'Ω':'W', 24 | 'Ά':'A', 'Έ':'E', 'Ί':'I', 'Ό':'O', 'Ύ':'Y', 'Ή':'H', 'Ώ':'W', 'Ϊ':'I', 25 | 'Ϋ':'Y' 26 | }; 27 | var TURKISH_MAP = { 28 | 'ş':'s', 'Ş':'S', 'ı':'i', 'İ':'I', 'ç':'c', 'Ç':'C', 'ü':'u', 'Ü':'U', 29 | 'ö':'o', 'Ö':'O', 'ğ':'g', 'Ğ':'G' 30 | }; 31 | var RUSSIAN_MAP = { 32 | 'а':'a', 'б':'b', 'в':'v', 'г':'g', 'д':'d', 'е':'e', 'ё':'yo', 'ж':'zh', 33 | 'з':'z', 'и':'i', 'й':'j', 'к':'k', 'л':'l', 'м':'m', 'н':'n', 'о':'o', 34 | 'п':'p', 'р':'r', 'с':'s', 'т':'t', 'у':'u', 'ф':'f', 'х':'h', 'ц':'c', 35 | 'ч':'ch', 'ш':'sh', 'щ':'sh', 'ъ':'', 'ы':'y', 'ь':'', 'э':'e', 'ю':'yu', 36 | 'я':'ya', 37 | 'А':'A', 'Б':'B', 'В':'V', 'Г':'G', 'Д':'D', 'Е':'E', 'Ё':'Yo', 'Ж':'Zh', 38 | 'З':'Z', 'И':'I', 'Й':'J', 'К':'K', 'Л':'L', 'М':'M', 'Н':'N', 'О':'O', 39 | 'П':'P', 'Р':'R', 'С':'S', 'Т':'T', 'У':'U', 'Ф':'F', 'Х':'H', 'Ц':'C', 40 | 'Ч':'Ch', 'Ш':'Sh', 'Щ':'Sh', 'Ъ':'', 'Ы':'Y', 'Ь':'', 'Э':'E', 'Ю':'Yu', 41 | 'Я':'Ya' 42 | }; 43 | var UKRAINIAN_MAP = { 44 | 'Є':'Ye', 'І':'I', 'Ї':'Yi', 'Ґ':'G', 'є':'ye', 'і':'i', 'ї':'yi', 'ґ':'g' 45 | }; 46 | var CZECH_MAP = { 47 | 'č':'c', 'ď':'d', 'ě':'e', 'ň': 'n', 'ř':'r', 'š':'s', 'ť':'t', 'ů':'u', 48 | 'ž':'z', 'Č':'C', 'Ď':'D', 'Ě':'E', 'Ň': 'N', 'Ř':'R', 'Š':'S', 'Ť':'T', 49 | 'Ů':'U', 'Ž':'Z' 50 | }; 51 | var POLISH_MAP = { 52 | 'ą':'a', 'ć':'c', 'ę':'e', 'ł':'l', 'ń':'n', 'ó':'o', 'ś':'s', 'ź':'z', 53 | 'ż':'z', 'Ą':'A', 'Ć':'C', 'Ę':'E', 'Ł':'L', 'Ń':'N', 'Ó':'O', 'Ś':'S', 54 | 'Ź':'Z', 'Ż':'Z' 55 | }; 56 | var LATVIAN_MAP = { 57 | 'ā':'a', 'č':'c', 'ē':'e', 'ģ':'g', 'ī':'i', 'ķ':'k', 'ļ':'l', 'ņ':'n', 58 | 'š':'s', 'ū':'u', 'ž':'z', 'Ā':'A', 'Č':'C', 'Ē':'E', 'Ģ':'G', 'Ī':'I', 59 | 'Ķ':'K', 'Ļ':'L', 'Ņ':'N', 'Š':'S', 'Ū':'U', 'Ž':'Z' 60 | }; 61 | var ARABIC_MAP = { 62 | 'أ':'a', 'ب':'b', 'ت':'t', 'ث': 'th', 'ج':'g', 'ح':'h', 'خ':'kh', 'د':'d', 63 | 'ذ':'th', 'ر':'r', 'ز':'z', 'س':'s', 'ش':'sh', 'ص':'s', 'ض':'d', 'ط':'t', 64 | 'ظ':'th', 'ع':'aa', 'غ':'gh', 'ف':'f', 'ق':'k', 'ك':'k', 'ل':'l', 'م':'m', 65 | 'ن':'n', 'ه':'h', 'و':'o', 'ي':'y' 66 | }; 67 | var LITHUANIAN_MAP = { 68 | 'ą':'a', 'č':'c', 'ę':'e', 'ė':'e', 'į':'i', 'š':'s', 'ų':'u', 'ū':'u', 69 | 'ž':'z', 70 | 'Ą':'A', 'Č':'C', 'Ę':'E', 'Ė':'E', 'Į':'I', 'Š':'S', 'Ų':'U', 'Ū':'U', 71 | 'Ž':'Z' 72 | }; 73 | var SERBIAN_MAP = { 74 | 'ђ':'dj', 'ј':'j', 'љ':'lj', 'њ':'nj', 'ћ':'c', 'џ':'dz', 'đ':'dj', 75 | 'Ђ':'Dj', 'Ј':'j', 'Љ':'Lj', 'Њ':'Nj', 'Ћ':'C', 'Џ':'Dz', 'Đ':'Dj' 76 | }; 77 | var AZERBAIJANI_MAP = { 78 | 'ç':'c', 'ə':'e', 'ğ':'g', 'ı':'i', 'ö':'o', 'ş':'s', 'ü':'u', 79 | 'Ç':'C', 'Ə':'E', 'Ğ':'G', 'İ':'I', 'Ö':'O', 'Ş':'S', 'Ü':'U' 80 | }; 81 | 82 | var ALL_DOWNCODE_MAPS = [ 83 | LATIN_MAP, 84 | LATIN_SYMBOLS_MAP, 85 | GREEK_MAP, 86 | TURKISH_MAP, 87 | RUSSIAN_MAP, 88 | UKRAINIAN_MAP, 89 | CZECH_MAP, 90 | POLISH_MAP, 91 | LATVIAN_MAP, 92 | ARABIC_MAP, 93 | LITHUANIAN_MAP, 94 | SERBIAN_MAP, 95 | AZERBAIJANI_MAP 96 | ]; 97 | 98 | var Downcoder = { 99 | 'Initialize': function() { 100 | if (Downcoder.map) { // already made 101 | return; 102 | } 103 | Downcoder.map = {}; 104 | Downcoder.chars = []; 105 | for (var i=0; i=0) && parseFloat(navigator.appVersion); 5 | var isIE = ((document.all) && (!isOpera)) && parseFloat(navigator.appVersion.split("MSIE ")[1].split(";")[0]); 6 | 7 | // Cross-browser event handlers. 8 | function addEvent(obj, evType, fn) { 9 | if (obj.addEventListener) { 10 | obj.addEventListener(evType, fn, false); 11 | return true; 12 | } else if (obj.attachEvent) { 13 | var r = obj.attachEvent("on" + evType, fn); 14 | return r; 15 | } else { 16 | return false; 17 | } 18 | } 19 | 20 | function removeEvent(obj, evType, fn) { 21 | if (obj.removeEventListener) { 22 | obj.removeEventListener(evType, fn, false); 23 | return true; 24 | } else if (obj.detachEvent) { 25 | obj.detachEvent("on" + evType, fn); 26 | return true; 27 | } else { 28 | return false; 29 | } 30 | } 31 | 32 | function cancelEventPropagation(e) { 33 | if (!e) e = window.event; 34 | e.cancelBubble = true; 35 | if (e.stopPropagation) e.stopPropagation(); 36 | } 37 | 38 | // quickElement(tagType, parentReference [, textInChildNode, attribute, attributeValue ...]); 39 | function quickElement() { 40 | var obj = document.createElement(arguments[0]); 41 | if (arguments[2]) { 42 | var textNode = document.createTextNode(arguments[2]); 43 | obj.appendChild(textNode); 44 | } 45 | var len = arguments.length; 46 | for (var i = 3; i < len; i += 2) { 47 | obj.setAttribute(arguments[i], arguments[i+1]); 48 | } 49 | arguments[1].appendChild(obj); 50 | return obj; 51 | } 52 | 53 | // "a" is reference to an object 54 | function removeChildren(a) { 55 | while (a.hasChildNodes()) a.removeChild(a.lastChild); 56 | } 57 | 58 | // ---------------------------------------------------------------------------- 59 | // Cross-browser xmlhttp object 60 | // from http://jibbering.com/2002/4/httprequest.html 61 | // ---------------------------------------------------------------------------- 62 | var xmlhttp; 63 | /*@cc_on @*/ 64 | /*@if (@_jscript_version >= 5) 65 | try { 66 | xmlhttp = new ActiveXObject("Msxml2.XMLHTTP"); 67 | } catch (e) { 68 | try { 69 | xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); 70 | } catch (E) { 71 | xmlhttp = false; 72 | } 73 | } 74 | @else 75 | xmlhttp = false; 76 | @end @*/ 77 | if (!xmlhttp && typeof XMLHttpRequest != 'undefined') { 78 | xmlhttp = new XMLHttpRequest(); 79 | } 80 | 81 | // ---------------------------------------------------------------------------- 82 | // Find-position functions by PPK 83 | // See http://www.quirksmode.org/js/findpos.html 84 | // ---------------------------------------------------------------------------- 85 | function findPosX(obj) { 86 | var curleft = 0; 87 | if (obj.offsetParent) { 88 | while (obj.offsetParent) { 89 | curleft += obj.offsetLeft - ((isOpera) ? 0 : obj.scrollLeft); 90 | obj = obj.offsetParent; 91 | } 92 | // IE offsetParent does not include the top-level 93 | if (isIE && obj.parentElement){ 94 | curleft += obj.offsetLeft - obj.scrollLeft; 95 | } 96 | } else if (obj.x) { 97 | curleft += obj.x; 98 | } 99 | return curleft; 100 | } 101 | 102 | function findPosY(obj) { 103 | var curtop = 0; 104 | if (obj.offsetParent) { 105 | while (obj.offsetParent) { 106 | curtop += obj.offsetTop - ((isOpera) ? 0 : obj.scrollTop); 107 | obj = obj.offsetParent; 108 | } 109 | // IE offsetParent does not include the top-level 110 | if (isIE && obj.parentElement){ 111 | curtop += obj.offsetTop - obj.scrollTop; 112 | } 113 | } else if (obj.y) { 114 | curtop += obj.y; 115 | } 116 | return curtop; 117 | } 118 | 119 | //----------------------------------------------------------------------------- 120 | // Date object extensions 121 | // ---------------------------------------------------------------------------- 122 | 123 | Date.prototype.getTwelveHours = function() { 124 | hours = this.getHours(); 125 | if (hours == 0) { 126 | return 12; 127 | } 128 | else { 129 | return hours <= 12 ? hours : hours-12 130 | } 131 | } 132 | 133 | Date.prototype.getTwoDigitMonth = function() { 134 | return (this.getMonth() < 9) ? '0' + (this.getMonth()+1) : (this.getMonth()+1); 135 | } 136 | 137 | Date.prototype.getTwoDigitDate = function() { 138 | return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate(); 139 | } 140 | 141 | Date.prototype.getTwoDigitTwelveHour = function() { 142 | return (this.getTwelveHours() < 10) ? '0' + this.getTwelveHours() : this.getTwelveHours(); 143 | } 144 | 145 | Date.prototype.getTwoDigitHour = function() { 146 | return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours(); 147 | } 148 | 149 | Date.prototype.getTwoDigitMinute = function() { 150 | return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes(); 151 | } 152 | 153 | Date.prototype.getTwoDigitSecond = function() { 154 | return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds(); 155 | } 156 | 157 | Date.prototype.getHourMinute = function() { 158 | return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute(); 159 | } 160 | 161 | Date.prototype.getHourMinuteSecond = function() { 162 | return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute() + ':' + this.getTwoDigitSecond(); 163 | } 164 | 165 | Date.prototype.strftime = function(format) { 166 | var fields = { 167 | c: this.toString(), 168 | d: this.getTwoDigitDate(), 169 | H: this.getTwoDigitHour(), 170 | I: this.getTwoDigitTwelveHour(), 171 | m: this.getTwoDigitMonth(), 172 | M: this.getTwoDigitMinute(), 173 | p: (this.getHours() >= 12) ? 'PM' : 'AM', 174 | S: this.getTwoDigitSecond(), 175 | w: '0' + this.getDay(), 176 | x: this.toLocaleDateString(), 177 | X: this.toLocaleTimeString(), 178 | y: ('' + this.getFullYear()).substr(2, 4), 179 | Y: '' + this.getFullYear(), 180 | '%' : '%' 181 | }; 182 | var result = '', i = 0; 183 | while (i < format.length) { 184 | if (format.charAt(i) === '%') { 185 | result = result + fields[format.charAt(i + 1)]; 186 | ++i; 187 | } 188 | else { 189 | result = result + format.charAt(i); 190 | } 191 | ++i; 192 | } 193 | return result; 194 | } 195 | 196 | // ---------------------------------------------------------------------------- 197 | // String object extensions 198 | // ---------------------------------------------------------------------------- 199 | String.prototype.pad_left = function(pad_length, pad_string) { 200 | var new_string = this; 201 | for (var i = 0; new_string.length < pad_length; i++) { 202 | new_string = pad_string + new_string; 203 | } 204 | return new_string; 205 | } 206 | 207 | String.prototype.strptime = function(format) { 208 | var split_format = format.split(/[.\-/]/); 209 | var date = this.split(/[.\-/]/); 210 | var i = 0; 211 | while (i < split_format.length) { 212 | switch (split_format[i]) { 213 | case "%d": 214 | var day = date[i]; 215 | break; 216 | case "%m": 217 | var month = date[i] - 1; 218 | break; 219 | case "%Y": 220 | var year = date[i]; 221 | break; 222 | case "%y": 223 | var year = date[i]; 224 | break; 225 | } 226 | ++i; 227 | }; 228 | return new Date(year, month, day); 229 | } 230 | 231 | // ---------------------------------------------------------------------------- 232 | // Get the computed style for and element 233 | // ---------------------------------------------------------------------------- 234 | function getStyle(oElm, strCssRule){ 235 | var strValue = ""; 236 | if(document.defaultView && document.defaultView.getComputedStyle){ 237 | strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule); 238 | } 239 | else if(oElm.currentStyle){ 240 | strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1){ 241 | return p1.toUpperCase(); 242 | }); 243 | strValue = oElm.currentStyle[strCssRule]; 244 | } 245 | return strValue; 246 | } 247 | -------------------------------------------------------------------------------- /static/admin/js/SelectFilter2.js: -------------------------------------------------------------------------------- 1 | /* 2 | SelectFilter2 - Turns a multiple-select box into a filter interface. 3 | 4 | Requires core.js, SelectBox.js and addevent.js. 5 | */ 6 | (function($) { 7 | function findForm(node) { 8 | // returns the node of the form containing the given node 9 | if (node.tagName.toLowerCase() != 'form') { 10 | return findForm(node.parentNode); 11 | } 12 | return node; 13 | } 14 | 15 | window.SelectFilter = { 16 | init: function(field_id, field_name, is_stacked, admin_static_prefix) { 17 | if (field_id.match(/__prefix__/)){ 18 | // Don't initialize on empty forms. 19 | return; 20 | } 21 | var from_box = document.getElementById(field_id); 22 | from_box.id += '_from'; // change its ID 23 | from_box.className = 'filtered'; 24 | 25 | var ps = from_box.parentNode.getElementsByTagName('p'); 26 | for (var i=0; i, because it just gets in the way. 29 | from_box.parentNode.removeChild(ps[i]); 30 | } else if (ps[i].className.indexOf("help") != -1) { 31 | // Move help text up to the top so it isn't below the select 32 | // boxes or wrapped off on the side to the right of the add 33 | // button: 34 | from_box.parentNode.insertBefore(ps[i], from_box.parentNode.firstChild); 35 | } 36 | } 37 | 38 | //
    or
    39 | var selector_div = quickElement('div', from_box.parentNode); 40 | selector_div.className = is_stacked ? 'selector stacked' : 'selector'; 41 | 42 | //
    43 | var selector_available = quickElement('div', selector_div); 44 | selector_available.className = 'selector-available'; 45 | var title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name])); 46 | quickElement('img', title_available, '', 'src', admin_static_prefix + 'img/icon-unknown.gif', 'width', '10', 'height', '10', 'class', 'help help-tooltip', 'title', interpolate(gettext('This is the list of available %s. You may choose some by selecting them in the box below and then clicking the "Choose" arrow between the two boxes.'), [field_name])); 47 | 48 | var filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter'); 49 | filter_p.className = 'selector-filter'; 50 | 51 | var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + "_input"); 52 | 53 | var search_selector_img = quickElement('img', search_filter_label, '', 'src', admin_static_prefix + 'img/selector-search.gif', 'class', 'help-tooltip', 'alt', '', 'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name])); 54 | 55 | filter_p.appendChild(document.createTextNode(' ')); 56 | 57 | var filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter")); 58 | filter_input.id = field_id + '_input'; 59 | 60 | selector_available.appendChild(from_box); 61 | var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_all_link'); 62 | choose_all.className = 'selector-chooseall'; 63 | 64 | //
      65 | var selector_chooser = quickElement('ul', selector_div); 66 | selector_chooser.className = 'selector-chooser'; 67 | var add_link = quickElement('a', quickElement('li', selector_chooser), gettext('Choose'), 'title', gettext('Choose'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_link'); 68 | add_link.className = 'selector-add'; 69 | var remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_to","' + field_id + '_from"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_link'); 70 | remove_link.className = 'selector-remove'; 71 | 72 | //
      73 | var selector_chosen = quickElement('div', selector_div); 74 | selector_chosen.className = 'selector-chosen'; 75 | var title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name])); 76 | quickElement('img', title_chosen, '', 'src', admin_static_prefix + 'img/icon-unknown.gif', 'width', '10', 'height', '10', 'class', 'help help-tooltip', 'title', interpolate(gettext('This is the list of chosen %s. You may remove some by selecting them in the box below and then clicking the "Remove" arrow between the two boxes.'), [field_name])); 77 | 78 | var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name')); 79 | to_box.className = 'filtered'; 80 | var clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_all_link'); 81 | clear_all.className = 'selector-clearall'; 82 | 83 | from_box.setAttribute('name', from_box.getAttribute('name') + '_old'); 84 | 85 | // Set up the JavaScript event handlers for the select box filter interface 86 | addEvent(filter_input, 'keypress', function(e) { SelectFilter.filter_key_press(e, field_id); }); 87 | addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); }); 88 | addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); }); 89 | addEvent(from_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) }); 90 | addEvent(to_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) }); 91 | addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); }); 92 | addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); }); 93 | addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); }); 94 | SelectBox.init(field_id + '_from'); 95 | SelectBox.init(field_id + '_to'); 96 | // Move selected from_box options to to_box 97 | SelectBox.move(field_id + '_from', field_id + '_to'); 98 | 99 | if (!is_stacked) { 100 | // In horizontal mode, give the same height to the two boxes. 101 | var j_from_box = $(from_box); 102 | var j_to_box = $(to_box); 103 | var resize_filters = function() { j_to_box.height($(filter_p).outerHeight() + j_from_box.outerHeight()); } 104 | if (j_from_box.outerHeight() > 0) { 105 | resize_filters(); // This fieldset is already open. Resize now. 106 | } else { 107 | // This fieldset is probably collapsed. Wait for its 'show' event. 108 | j_to_box.closest('fieldset').one('show.fieldset', resize_filters); 109 | } 110 | } 111 | 112 | // Initial icon refresh 113 | SelectFilter.refresh_icons(field_id); 114 | }, 115 | refresh_icons: function(field_id) { 116 | var from = $('#' + field_id + '_from'); 117 | var to = $('#' + field_id + '_to'); 118 | var is_from_selected = from.find('option:selected').length > 0; 119 | var is_to_selected = to.find('option:selected').length > 0; 120 | // Active if at least one item is selected 121 | $('#' + field_id + '_add_link').toggleClass('active', is_from_selected); 122 | $('#' + field_id + '_remove_link').toggleClass('active', is_to_selected); 123 | // Active if the corresponding box isn't empty 124 | $('#' + field_id + '_add_all_link').toggleClass('active', from.find('option').length > 0); 125 | $('#' + field_id + '_remove_all_link').toggleClass('active', to.find('option').length > 0); 126 | }, 127 | filter_key_press: function(event, field_id) { 128 | var from = document.getElementById(field_id + '_from'); 129 | // don't submit form if user pressed Enter 130 | if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) { 131 | from.selectedIndex = 0; 132 | SelectBox.move(field_id + '_from', field_id + '_to'); 133 | from.selectedIndex = 0; 134 | event.preventDefault() 135 | return false; 136 | } 137 | }, 138 | filter_key_up: function(event, field_id) { 139 | var from = document.getElementById(field_id + '_from'); 140 | var temp = from.selectedIndex; 141 | SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value); 142 | from.selectedIndex = temp; 143 | return true; 144 | }, 145 | filter_key_down: function(event, field_id) { 146 | var from = document.getElementById(field_id + '_from'); 147 | // right arrow -- move across 148 | if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) { 149 | var old_index = from.selectedIndex; 150 | SelectBox.move(field_id + '_from', field_id + '_to'); 151 | from.selectedIndex = (old_index == from.length) ? from.length - 1 : old_index; 152 | return false; 153 | } 154 | // down arrow -- wrap around 155 | if ((event.which && event.which == 40) || (event.keyCode && event.keyCode == 40)) { 156 | from.selectedIndex = (from.length == from.selectedIndex + 1) ? 0 : from.selectedIndex + 1; 157 | } 158 | // up arrow -- wrap around 159 | if ((event.which && event.which == 38) || (event.keyCode && event.keyCode == 38)) { 160 | from.selectedIndex = (from.selectedIndex == 0) ? from.length - 1 : from.selectedIndex - 1; 161 | } 162 | return true; 163 | } 164 | } 165 | 166 | })(django.jQuery); 167 | -------------------------------------------------------------------------------- /static/admin/js/inlines.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Django admin inlines 3 | * 4 | * Based on jQuery Formset 1.1 5 | * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com) 6 | * @requires jQuery 1.2.6 or later 7 | * 8 | * Copyright (c) 2009, Stanislaus Madueke 9 | * All rights reserved. 10 | * 11 | * Spiced up with Code from Zain Memon's GSoC project 2009 12 | * and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip. 13 | * 14 | * Licensed under the New BSD License 15 | * See: http://www.opensource.org/licenses/bsd-license.php 16 | */ 17 | (function($) { 18 | $.fn.formset = function(opts) { 19 | var options = $.extend({}, $.fn.formset.defaults, opts); 20 | var $this = $(this); 21 | var $parent = $this.parent(); 22 | var updateElementIndex = function(el, prefix, ndx) { 23 | var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); 24 | var replacement = prefix + "-" + ndx; 25 | if ($(el).prop("for")) { 26 | $(el).prop("for", $(el).prop("for").replace(id_regex, replacement)); 27 | } 28 | if (el.id) { 29 | el.id = el.id.replace(id_regex, replacement); 30 | } 31 | if (el.name) { 32 | el.name = el.name.replace(id_regex, replacement); 33 | } 34 | }; 35 | var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off"); 36 | var nextIndex = parseInt(totalForms.val(), 10); 37 | var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off"); 38 | // only show the add button if we are allowed to add more items, 39 | // note that max_num = None translates to a blank string. 40 | var showAddButton = maxForms.val() === '' || (maxForms.val()-totalForms.val()) > 0; 41 | $this.each(function(i) { 42 | $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); 43 | }); 44 | if ($this.length && showAddButton) { 45 | var addButton; 46 | if ($this.prop("tagName") == "TR") { 47 | // If forms are laid out as table rows, insert the 48 | // "add" button in a new table row: 49 | var numCols = this.eq(-1).children().length; 50 | $parent.append('' + options.addText + ""); 51 | addButton = $parent.find("tr:last a"); 52 | } else { 53 | // Otherwise, insert it immediately after the last form: 54 | $this.filter(":last").after('"); 55 | addButton = $this.filter(":last").next().find("a"); 56 | } 57 | addButton.click(function(e) { 58 | e.preventDefault(); 59 | var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS"); 60 | var template = $("#" + options.prefix + "-empty"); 61 | var row = template.clone(true); 62 | row.removeClass(options.emptyCssClass) 63 | .addClass(options.formCssClass) 64 | .attr("id", options.prefix + "-" + nextIndex); 65 | if (row.is("tr")) { 66 | // If the forms are laid out in table rows, insert 67 | // the remove button into the last table cell: 68 | row.children(":last").append('"); 69 | } else if (row.is("ul") || row.is("ol")) { 70 | // If they're laid out as an ordered/unordered list, 71 | // insert an
    • after the last list item: 72 | row.append('
    • ' + options.deleteText + "
    • "); 73 | } else { 74 | // Otherwise, just insert the remove button as the 75 | // last child element of the form's container: 76 | row.children(":first").append('' + options.deleteText + ""); 77 | } 78 | row.find("*").each(function() { 79 | updateElementIndex(this, options.prefix, totalForms.val()); 80 | }); 81 | // Insert the new form when it has been fully edited 82 | row.insertBefore($(template)); 83 | // Update number of total forms 84 | $(totalForms).val(parseInt(totalForms.val(), 10) + 1); 85 | nextIndex += 1; 86 | // Hide add button in case we've hit the max, except we want to add infinitely 87 | if ((maxForms.val() !== '') && (maxForms.val()-totalForms.val()) <= 0) { 88 | addButton.parent().hide(); 89 | } 90 | // The delete button of each row triggers a bunch of other things 91 | row.find("a." + options.deleteCssClass).click(function(e) { 92 | e.preventDefault(); 93 | // Remove the parent form containing this button: 94 | var row = $(this).parents("." + options.formCssClass); 95 | row.remove(); 96 | nextIndex -= 1; 97 | // If a post-delete callback was provided, call it with the deleted form: 98 | if (options.removed) { 99 | options.removed(row); 100 | } 101 | // Update the TOTAL_FORMS form count. 102 | var forms = $("." + options.formCssClass); 103 | $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); 104 | // Show add button again once we drop below max 105 | if ((maxForms.val() === '') || (maxForms.val()-forms.length) > 0) { 106 | addButton.parent().show(); 107 | } 108 | // Also, update names and ids for all remaining form controls 109 | // so they remain in sequence: 110 | for (var i=0, formCount=forms.length; itr>td.order-cell { 408 | display: none; 409 | } 410 | 411 | /* FORM DEFAULTS */ 412 | 413 | input, textarea, select, .form-row p, form .button { 414 | margin: 2px 0; 415 | padding: 2px 3px; 416 | vertical-align: middle; 417 | font-family: "Lucida Grande", Verdana, Arial, sans-serif; 418 | font-weight: normal; 419 | font-size: 11px; 420 | } 421 | 422 | textarea { 423 | vertical-align: top !important; 424 | } 425 | 426 | input[type=text], input[type=password], input[type=email], input[type=url], input[type=number], 427 | textarea, select, .vTextField { 428 | border: 1px solid #ccc; 429 | } 430 | 431 | /* FORM BUTTONS */ 432 | 433 | .button, input[type=submit], input[type=button], .submit-row input, a.button { 434 | background: #fff url(../img/nav-bg.gif) bottom repeat-x; 435 | padding: 3px 5px; 436 | color: black; 437 | border: 1px solid #bbb; 438 | border-color: #ddd #aaa #aaa #ddd; 439 | } 440 | 441 | a.button { 442 | padding: 4px 5px; 443 | } 444 | 445 | .button:active, input[type=submit]:active, input[type=button]:active { 446 | background-image: url(../img/nav-bg-reverse.gif); 447 | background-position: top; 448 | } 449 | 450 | .button[disabled], input[type=submit][disabled], input[type=button][disabled] { 451 | background-image: url(../img/nav-bg.gif); 452 | background-position: bottom; 453 | opacity: 0.4; 454 | } 455 | 456 | .button.default, input[type=submit].default, .submit-row input.default { 457 | border: 2px solid #5b80b2; 458 | background: #7CA0C7 url(../img/default-bg.gif) bottom repeat-x; 459 | font-weight: bold; 460 | color: #fff; 461 | float: right; 462 | } 463 | 464 | .button.default:active, input[type=submit].default:active { 465 | background-image: url(../img/default-bg-reverse.gif); 466 | background-position: top; 467 | } 468 | 469 | .button[disabled].default, input[type=submit][disabled].default, input[type=button][disabled].default { 470 | background-image: url(../img/default-bg.gif); 471 | background-position: bottom; 472 | opacity: 0.4; 473 | } 474 | 475 | 476 | /* MODULES */ 477 | 478 | .module { 479 | border: 1px solid #ccc; 480 | margin-bottom: 5px; 481 | background: #fff; 482 | } 483 | 484 | .module p, .module ul, .module h3, .module h4, .module dl, .module pre { 485 | padding-left: 10px; 486 | padding-right: 10px; 487 | } 488 | 489 | .module blockquote { 490 | margin-left: 12px; 491 | } 492 | 493 | .module ul, .module ol { 494 | margin-left: 1.5em; 495 | } 496 | 497 | .module h3 { 498 | margin-top: .6em; 499 | } 500 | 501 | .module h2, .module caption, .inline-group h2 { 502 | margin: 0; 503 | padding: 2px 5px 3px 5px; 504 | font-size: 11px; 505 | text-align: left; 506 | font-weight: bold; 507 | background: #7CA0C7 url(../img/default-bg.gif) top left repeat-x; 508 | color: #fff; 509 | } 510 | 511 | .module table { 512 | border-collapse: collapse; 513 | } 514 | 515 | /* MESSAGES & ERRORS */ 516 | 517 | ul.messagelist { 518 | padding: 0; 519 | margin: 0; 520 | } 521 | 522 | ul.messagelist li { 523 | font-size: 12px; 524 | font-weight: bold; 525 | display: block; 526 | padding: 5px 5px 4px 25px; 527 | margin: 0 0 3px 0; 528 | border-bottom: 1px solid #ddd; 529 | color: #666; 530 | background: #dfd url(../img/icon_success.gif) 5px .3em no-repeat; 531 | } 532 | 533 | ul.messagelist li.warning { 534 | background: #ffc url(../img/icon_alert.gif) 5px .3em no-repeat; 535 | } 536 | 537 | ul.messagelist li.error { 538 | background: #ffefef url(../img/icon_error.gif) 5px .3em no-repeat; 539 | } 540 | 541 | .errornote { 542 | font-size: 12px !important; 543 | font-weight: bold; 544 | display: block; 545 | padding: 5px 5px 4px 25px; 546 | margin: 0 0 3px 0; 547 | border: 1px solid #c22; 548 | color: #c11; 549 | background: #ffefef url(../img/icon_error.gif) 5px .38em no-repeat; 550 | } 551 | 552 | .errornote, ul.errorlist { 553 | border-radius: 1px; 554 | } 555 | 556 | ul.errorlist { 557 | margin: 0 0 4px !important; 558 | padding: 0 !important; 559 | color: #fff; 560 | background: #c11; 561 | } 562 | 563 | ul.errorlist li { 564 | font-size: 12px !important; 565 | display: block; 566 | padding: 5px 5px 4px 7px; 567 | margin: 3px 0 0 0; 568 | } 569 | 570 | ul.errorlist li:first-child { 571 | margin-top: 0; 572 | } 573 | 574 | ul.errorlist li a { 575 | color: #fff; 576 | text-decoration: underline; 577 | } 578 | 579 | td ul.errorlist { 580 | margin: 0 !important; 581 | padding: 0 !important; 582 | } 583 | 584 | td ul.errorlist li { 585 | margin: 0 !important; 586 | } 587 | 588 | .errors, .form-row.errors { 589 | background: #ffefef; 590 | } 591 | 592 | .form-row.errors { 593 | border: 1px solid #c22; 594 | margin: -1px; 595 | } 596 | 597 | .errors input, .errors select, .errors textarea { 598 | border: 1px solid #c11; 599 | } 600 | 601 | div.system-message { 602 | background: #ffc; 603 | margin: 10px; 604 | padding: 6px 8px; 605 | font-size: .8em; 606 | } 607 | 608 | div.system-message p.system-message-title { 609 | padding: 4px 5px 4px 25px; 610 | margin: 0; 611 | color: #c11; 612 | background: #ffefef url(../img/icon_error.gif) 5px .3em no-repeat; 613 | } 614 | 615 | .description { 616 | font-size: 12px; 617 | padding: 5px 0 0 12px; 618 | } 619 | 620 | /* BREADCRUMBS */ 621 | 622 | div.breadcrumbs { 623 | background: #fff url(../img/nav-bg-reverse.gif) 0 -10px repeat-x; 624 | padding: 2px 8px 3px 8px; 625 | font-size: 11px; 626 | color: #999; 627 | border-top: 1px solid #fff; 628 | border-bottom: 1px solid #ddd; 629 | text-align: left; 630 | } 631 | 632 | /* ACTION ICONS */ 633 | 634 | .addlink { 635 | padding-left: 12px; 636 | background: url(../img/icon_addlink.gif) 0 .2em no-repeat; 637 | } 638 | 639 | .changelink, .inlinechangelink { 640 | padding-left: 12px; 641 | background: url(../img/icon_changelink.gif) 0 .2em no-repeat; 642 | } 643 | 644 | .deletelink { 645 | padding-left: 12px; 646 | background: url(../img/icon_deletelink.gif) 0 .25em no-repeat; 647 | } 648 | 649 | a.deletelink:link, a.deletelink:visited { 650 | color: #CC3434; 651 | } 652 | 653 | a.deletelink:hover { 654 | color: #993333; 655 | } 656 | 657 | /* OBJECT TOOLS */ 658 | 659 | .object-tools { 660 | font-size: 10px; 661 | font-weight: bold; 662 | font-family: Arial,Helvetica,sans-serif; 663 | padding-left: 0; 664 | float: right; 665 | position: relative; 666 | margin-top: -2.4em; 667 | margin-bottom: -2em; 668 | } 669 | 670 | .form-row .object-tools { 671 | margin-top: 5px; 672 | margin-bottom: 5px; 673 | float: none; 674 | height: 2em; 675 | padding-left: 3.5em; 676 | } 677 | 678 | .object-tools li { 679 | display: block; 680 | float: left; 681 | margin-left: 5px; 682 | height: 16px; 683 | } 684 | 685 | .object-tools a { 686 | border-radius: 15px; 687 | } 688 | 689 | .object-tools a:link, .object-tools a:visited { 690 | display: block; 691 | float: left; 692 | color: #fff; 693 | padding: .2em 10px; 694 | background: #999; 695 | } 696 | 697 | .object-tools a:hover, .object-tools li:hover a { 698 | background-color: #5b80b2; 699 | } 700 | 701 | .object-tools a.viewsitelink, .object-tools a.golink { 702 | background: #999 url(../img/tooltag-arrowright.png) 95% center no-repeat; 703 | padding-right: 26px; 704 | } 705 | 706 | .object-tools a.addlink { 707 | background: #999 url(../img/tooltag-add.png) 95% center no-repeat; 708 | padding-right: 26px; 709 | } 710 | 711 | /* OBJECT HISTORY */ 712 | 713 | table#change-history { 714 | width: 100%; 715 | } 716 | 717 | table#change-history tbody th { 718 | width: 16em; 719 | } 720 | 721 | /* PAGE STRUCTURE */ 722 | 723 | #container { 724 | position: relative; 725 | width: 100%; 726 | min-width: 760px; 727 | padding: 0; 728 | } 729 | 730 | #content { 731 | margin: 10px 15px; 732 | } 733 | 734 | #content-main { 735 | float: left; 736 | width: 100%; 737 | } 738 | 739 | #content-related { 740 | float: right; 741 | width: 18em; 742 | position: relative; 743 | margin-right: -19em; 744 | } 745 | 746 | #footer { 747 | clear: both; 748 | padding: 10px; 749 | } 750 | 751 | /* COLUMN TYPES */ 752 | 753 | .colMS { 754 | margin-right: 20em !important; 755 | } 756 | 757 | .colSM { 758 | margin-left: 20em !important; 759 | } 760 | 761 | .colSM #content-related { 762 | float: left; 763 | margin-right: 0; 764 | margin-left: -19em; 765 | } 766 | 767 | .colSM #content-main { 768 | float: right; 769 | } 770 | 771 | .popup .colM { 772 | width: 95%; 773 | } 774 | 775 | .subcol { 776 | float: left; 777 | width: 46%; 778 | margin-right: 15px; 779 | } 780 | 781 | .dashboard #content { 782 | width: 500px; 783 | } 784 | 785 | /* HEADER */ 786 | 787 | #header { 788 | width: 100%; 789 | background: #417690; 790 | color: #ffc; 791 | overflow: hidden; 792 | } 793 | 794 | #header a:link, #header a:visited { 795 | color: #fff; 796 | } 797 | 798 | #header a:hover { 799 | text-decoration: underline; 800 | } 801 | 802 | #branding { 803 | float: left; 804 | } 805 | #branding h1 { 806 | padding: 0 10px; 807 | font-size: 18px; 808 | margin: 8px 0; 809 | font-weight: normal; 810 | } 811 | 812 | #branding h1, #branding h1 a:link, #branding h1 a:visited { 813 | color: #f4f379; 814 | } 815 | 816 | #branding h2 { 817 | padding: 0 10px; 818 | font-size: 14px; 819 | margin: -8px 0 8px 0; 820 | font-weight: normal; 821 | color: #ffc; 822 | } 823 | 824 | #branding a:hover { 825 | text-decoration: none; 826 | } 827 | 828 | #user-tools { 829 | float: right; 830 | padding: 1.2em 10px; 831 | font-size: 11px; 832 | text-align: right; 833 | } 834 | 835 | /* SIDEBAR */ 836 | 837 | #content-related h3 { 838 | font-size: 12px; 839 | color: #666; 840 | margin-bottom: 3px; 841 | } 842 | 843 | #content-related h4 { 844 | font-size: 11px; 845 | } 846 | 847 | #content-related .module h2 { 848 | background: #eee url(../img/nav-bg.gif) bottom left repeat-x; 849 | color: #666; 850 | } 851 | -------------------------------------------------------------------------------- /static/admin/js/admin/DateTimeShortcuts.js: -------------------------------------------------------------------------------- 1 | // Inserts shortcut buttons after all of the following: 2 | // 3 | // 4 | 5 | var DateTimeShortcuts = { 6 | calendars: [], 7 | calendarInputs: [], 8 | clockInputs: [], 9 | dismissClockFunc: [], 10 | dismissCalendarFunc: [], 11 | calendarDivName1: 'calendarbox', // name of calendar
      that gets toggled 12 | calendarDivName2: 'calendarin', // name of
      that contains calendar 13 | calendarLinkName: 'calendarlink',// name of the link that is used to toggle 14 | clockDivName: 'clockbox', // name of clock
      that gets toggled 15 | clockLinkName: 'clocklink', // name of the link that is used to toggle 16 | shortCutsClass: 'datetimeshortcuts', // class of the clock and cal shortcuts 17 | timezoneWarningClass: 'timezonewarning', // class of the warning for timezone mismatch 18 | timezoneOffset: 0, 19 | admin_media_prefix: '', 20 | init: function() { 21 | // Get admin_media_prefix by grabbing it off the window object. It's 22 | // set in the admin/base.html template, so if it's not there, someone's 23 | // overridden the template. In that case, we'll set a clearly-invalid 24 | // value in the hopes that someone will examine HTTP requests and see it. 25 | if (window.__admin_media_prefix__ != undefined) { 26 | DateTimeShortcuts.admin_media_prefix = window.__admin_media_prefix__; 27 | } else { 28 | DateTimeShortcuts.admin_media_prefix = '/missing-admin-media-prefix/'; 29 | } 30 | 31 | if (window.__admin_utc_offset__ != undefined) { 32 | var serverOffset = window.__admin_utc_offset__; 33 | var localOffset = new Date().getTimezoneOffset() * -60; 34 | DateTimeShortcuts.timezoneOffset = localOffset - serverOffset; 35 | } 36 | 37 | var inputs = document.getElementsByTagName('input'); 38 | for (i=0; i 0) { 78 | message = ngettext( 79 | 'Note: You are %s hour ahead of server time.', 80 | 'Note: You are %s hours ahead of server time.', 81 | timezoneOffset 82 | ); 83 | } 84 | else { 85 | timezoneOffset *= -1 86 | message = ngettext( 87 | 'Note: You are %s hour behind server time.', 88 | 'Note: You are %s hours behind server time.', 89 | timezoneOffset 90 | ); 91 | } 92 | message = interpolate(message, [timezoneOffset]); 93 | 94 | var $warning = $(''); 95 | $warning.attr('class', warningClass); 96 | $warning.text(message); 97 | 98 | $(inp).parent() 99 | .append($('
      ')) 100 | .append($warning) 101 | }, 102 | // Add clock widget to a given field 103 | addClock: function(inp) { 104 | var num = DateTimeShortcuts.clockInputs.length; 105 | DateTimeShortcuts.clockInputs[num] = inp; 106 | DateTimeShortcuts.dismissClockFunc[num] = function() { DateTimeShortcuts.dismissClock(num); return true; }; 107 | 108 | // Shortcut links (clock icon and "Now" link) 109 | var shortcuts_span = document.createElement('span'); 110 | shortcuts_span.className = DateTimeShortcuts.shortCutsClass; 111 | inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); 112 | var now_link = document.createElement('a'); 113 | now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", -1);"); 114 | now_link.appendChild(document.createTextNode(gettext('Now'))); 115 | var clock_link = document.createElement('a'); 116 | clock_link.setAttribute('href', 'javascript:DateTimeShortcuts.openClock(' + num + ');'); 117 | clock_link.id = DateTimeShortcuts.clockLinkName + num; 118 | quickElement('img', clock_link, '', 'src', DateTimeShortcuts.admin_media_prefix + 'img/icon_clock.gif', 'alt', gettext('Clock')); 119 | shortcuts_span.appendChild(document.createTextNode('\240')); 120 | shortcuts_span.appendChild(now_link); 121 | shortcuts_span.appendChild(document.createTextNode('\240|\240')); 122 | shortcuts_span.appendChild(clock_link); 123 | 124 | // Create clock link div 125 | // 126 | // Markup looks like: 127 | //
      128 | //

      Choose a time

      129 | // 135 | //

      Cancel

      136 | //
      137 | 138 | var clock_box = document.createElement('div'); 139 | clock_box.style.display = 'none'; 140 | clock_box.style.position = 'absolute'; 141 | clock_box.className = 'clockbox module'; 142 | clock_box.setAttribute('id', DateTimeShortcuts.clockDivName + num); 143 | document.body.appendChild(clock_box); 144 | addEvent(clock_box, 'click', cancelEventPropagation); 145 | 146 | quickElement('h2', clock_box, gettext('Choose a time')); 147 | var time_list = quickElement('ul', clock_box); 148 | time_list.className = 'timelist'; 149 | quickElement("a", quickElement("li", time_list), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", -1);"); 150 | quickElement("a", quickElement("li", time_list), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 0);"); 151 | quickElement("a", quickElement("li", time_list), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 6);"); 152 | quickElement("a", quickElement("li", time_list), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 12);"); 153 | 154 | var cancel_p = quickElement('p', clock_box); 155 | cancel_p.className = 'calendar-cancel'; 156 | quickElement('a', cancel_p, gettext('Cancel'), 'href', 'javascript:DateTimeShortcuts.dismissClock(' + num + ');'); 157 | django.jQuery(document).bind('keyup', function(event) { 158 | if (event.which == 27) { 159 | // ESC key closes popup 160 | DateTimeShortcuts.dismissClock(num); 161 | event.preventDefault(); 162 | } 163 | }); 164 | }, 165 | openClock: function(num) { 166 | var clock_box = document.getElementById(DateTimeShortcuts.clockDivName+num) 167 | var clock_link = document.getElementById(DateTimeShortcuts.clockLinkName+num) 168 | 169 | // Recalculate the clockbox position 170 | // is it left-to-right or right-to-left layout ? 171 | if (getStyle(document.body,'direction')!='rtl') { 172 | clock_box.style.left = findPosX(clock_link) + 17 + 'px'; 173 | } 174 | else { 175 | // since style's width is in em, it'd be tough to calculate 176 | // px value of it. let's use an estimated px for now 177 | // TODO: IE returns wrong value for findPosX when in rtl mode 178 | // (it returns as it was left aligned), needs to be fixed. 179 | clock_box.style.left = findPosX(clock_link) - 110 + 'px'; 180 | } 181 | clock_box.style.top = Math.max(0, findPosY(clock_link) - 30) + 'px'; 182 | 183 | // Show the clock box 184 | clock_box.style.display = 'block'; 185 | addEvent(document, 'click', DateTimeShortcuts.dismissClockFunc[num]); 186 | }, 187 | dismissClock: function(num) { 188 | document.getElementById(DateTimeShortcuts.clockDivName + num).style.display = 'none'; 189 | removeEvent(document, 'click', DateTimeShortcuts.dismissClockFunc[num]); 190 | }, 191 | handleClockQuicklink: function(num, val) { 192 | var d; 193 | if (val == -1) { 194 | d = DateTimeShortcuts.now(); 195 | } 196 | else { 197 | d = new Date(1970, 1, 1, val, 0, 0, 0) 198 | } 199 | DateTimeShortcuts.clockInputs[num].value = d.strftime(get_format('TIME_INPUT_FORMATS')[0]); 200 | DateTimeShortcuts.clockInputs[num].focus(); 201 | DateTimeShortcuts.dismissClock(num); 202 | }, 203 | // Add calendar widget to a given field. 204 | addCalendar: function(inp) { 205 | var num = DateTimeShortcuts.calendars.length; 206 | 207 | DateTimeShortcuts.calendarInputs[num] = inp; 208 | DateTimeShortcuts.dismissCalendarFunc[num] = function() { DateTimeShortcuts.dismissCalendar(num); return true; }; 209 | 210 | // Shortcut links (calendar icon and "Today" link) 211 | var shortcuts_span = document.createElement('span'); 212 | shortcuts_span.className = DateTimeShortcuts.shortCutsClass; 213 | inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); 214 | var today_link = document.createElement('a'); 215 | today_link.setAttribute('href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', 0);'); 216 | today_link.appendChild(document.createTextNode(gettext('Today'))); 217 | var cal_link = document.createElement('a'); 218 | cal_link.setAttribute('href', 'javascript:DateTimeShortcuts.openCalendar(' + num + ');'); 219 | cal_link.id = DateTimeShortcuts.calendarLinkName + num; 220 | quickElement('img', cal_link, '', 'src', DateTimeShortcuts.admin_media_prefix + 'img/icon_calendar.gif', 'alt', gettext('Calendar')); 221 | shortcuts_span.appendChild(document.createTextNode('\240')); 222 | shortcuts_span.appendChild(today_link); 223 | shortcuts_span.appendChild(document.createTextNode('\240|\240')); 224 | shortcuts_span.appendChild(cal_link); 225 | 226 | // Create calendarbox div. 227 | // 228 | // Markup looks like: 229 | // 230 | //
      231 | //

      232 | // 233 | // February 2003 234 | //

      235 | //
      236 | // 237 | //
      238 | //
      239 | // Yesterday | Today | Tomorrow 240 | //
      241 | //

      Cancel

      242 | //
      243 | var cal_box = document.createElement('div'); 244 | cal_box.style.display = 'none'; 245 | cal_box.style.position = 'absolute'; 246 | cal_box.className = 'calendarbox module'; 247 | cal_box.setAttribute('id', DateTimeShortcuts.calendarDivName1 + num); 248 | document.body.appendChild(cal_box); 249 | addEvent(cal_box, 'click', cancelEventPropagation); 250 | 251 | // next-prev links 252 | var cal_nav = quickElement('div', cal_box); 253 | var cal_nav_prev = quickElement('a', cal_nav, '<', 'href', 'javascript:DateTimeShortcuts.drawPrev('+num+');'); 254 | cal_nav_prev.className = 'calendarnav-previous'; 255 | var cal_nav_next = quickElement('a', cal_nav, '>', 'href', 'javascript:DateTimeShortcuts.drawNext('+num+');'); 256 | cal_nav_next.className = 'calendarnav-next'; 257 | 258 | // main box 259 | var cal_main = quickElement('div', cal_box, '', 'id', DateTimeShortcuts.calendarDivName2 + num); 260 | cal_main.className = 'calendar'; 261 | DateTimeShortcuts.calendars[num] = new Calendar(DateTimeShortcuts.calendarDivName2 + num, DateTimeShortcuts.handleCalendarCallback(num)); 262 | DateTimeShortcuts.calendars[num].drawCurrent(); 263 | 264 | // calendar shortcuts 265 | var shortcuts = quickElement('div', cal_box); 266 | shortcuts.className = 'calendar-shortcuts'; 267 | quickElement('a', shortcuts, gettext('Yesterday'), 'href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', -1);'); 268 | shortcuts.appendChild(document.createTextNode('\240|\240')); 269 | quickElement('a', shortcuts, gettext('Today'), 'href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', 0);'); 270 | shortcuts.appendChild(document.createTextNode('\240|\240')); 271 | quickElement('a', shortcuts, gettext('Tomorrow'), 'href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', +1);'); 272 | 273 | // cancel bar 274 | var cancel_p = quickElement('p', cal_box); 275 | cancel_p.className = 'calendar-cancel'; 276 | quickElement('a', cancel_p, gettext('Cancel'), 'href', 'javascript:DateTimeShortcuts.dismissCalendar(' + num + ');'); 277 | django.jQuery(document).bind('keyup', function(event) { 278 | if (event.which == 27) { 279 | // ESC key closes popup 280 | DateTimeShortcuts.dismissCalendar(num); 281 | event.preventDefault(); 282 | } 283 | }); 284 | }, 285 | openCalendar: function(num) { 286 | var cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1+num) 287 | var cal_link = document.getElementById(DateTimeShortcuts.calendarLinkName+num) 288 | var inp = DateTimeShortcuts.calendarInputs[num]; 289 | 290 | // Determine if the current value in the input has a valid date. 291 | // If so, draw the calendar with that date's year and month. 292 | if (inp.value) { 293 | var format = get_format('DATE_INPUT_FORMATS')[0]; 294 | var selected = inp.value.strptime(format); 295 | var year = selected.getFullYear(); 296 | var month = selected.getMonth() + 1; 297 | var re = /\d{4}/ 298 | if (re.test(year.toString()) && month >= 1 && month <= 12) { 299 | DateTimeShortcuts.calendars[num].drawDate(month, year, selected); 300 | } 301 | } 302 | 303 | // Recalculate the clockbox position 304 | // is it left-to-right or right-to-left layout ? 305 | if (getStyle(document.body,'direction')!='rtl') { 306 | cal_box.style.left = findPosX(cal_link) + 17 + 'px'; 307 | } 308 | else { 309 | // since style's width is in em, it'd be tough to calculate 310 | // px value of it. let's use an estimated px for now 311 | // TODO: IE returns wrong value for findPosX when in rtl mode 312 | // (it returns as it was left aligned), needs to be fixed. 313 | cal_box.style.left = findPosX(cal_link) - 180 + 'px'; 314 | } 315 | cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + 'px'; 316 | 317 | cal_box.style.display = 'block'; 318 | addEvent(document, 'click', DateTimeShortcuts.dismissCalendarFunc[num]); 319 | }, 320 | dismissCalendar: function(num) { 321 | document.getElementById(DateTimeShortcuts.calendarDivName1+num).style.display = 'none'; 322 | removeEvent(document, 'click', DateTimeShortcuts.dismissCalendarFunc[num]); 323 | }, 324 | drawPrev: function(num) { 325 | DateTimeShortcuts.calendars[num].drawPreviousMonth(); 326 | }, 327 | drawNext: function(num) { 328 | DateTimeShortcuts.calendars[num].drawNextMonth(); 329 | }, 330 | handleCalendarCallback: function(num) { 331 | var format = get_format('DATE_INPUT_FORMATS')[0]; 332 | // the format needs to be escaped a little 333 | format = format.replace('\\', '\\\\'); 334 | format = format.replace('\r', '\\r'); 335 | format = format.replace('\n', '\\n'); 336 | format = format.replace('\t', '\\t'); 337 | format = format.replace("'", "\\'"); 338 | return ["function(y, m, d) { DateTimeShortcuts.calendarInputs[", 339 | num, 340 | "].value = new Date(y, m-1, d).strftime('", 341 | format, 342 | "');DateTimeShortcuts.calendarInputs[", 343 | num, 344 | "].focus();document.getElementById(DateTimeShortcuts.calendarDivName1+", 345 | num, 346 | ").style.display='none';}"].join(''); 347 | }, 348 | handleCalendarQuickLink: function(num, offset) { 349 | var d = DateTimeShortcuts.now(); 350 | d.setDate(d.getDate() + offset) 351 | DateTimeShortcuts.calendarInputs[num].value = d.strftime(get_format('DATE_INPUT_FORMATS')[0]); 352 | DateTimeShortcuts.calendarInputs[num].focus(); 353 | DateTimeShortcuts.dismissCalendar(num); 354 | } 355 | } 356 | 357 | addEvent(window, 'load', DateTimeShortcuts.init); 358 | --------------------------------------------------------------------------------