summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2010-09-26 17:44:57 -0400
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2010-09-26 17:44:57 -0400
commit35954df3196e46653b6b66ea80fe95dfacc7484d (patch)
treef3949a46037f2f162639b3bbe604013491ba06dc
parent98360b7821240c21ec9b7adde155e4bcaf7bfdd5 (diff)
downloadaskbot-35954df3196e46653b6b66ea80fe95dfacc7484d.tar.gz
askbot-35954df3196e46653b6b66ea80fe95dfacc7484d.tar.bz2
askbot-35954df3196e46653b6b66ea80fe95dfacc7484d.zip
stated working on switch to jinja
-rw-r--r--askbot/models/__init__.py44
-rw-r--r--askbot/skins/default/templates/ask_form.jinja.html62
-rw-r--r--askbot/skins/default/templates/base.jinja.html126
-rw-r--r--askbot/skins/default/templates/footer.jinja.html44
-rw-r--r--askbot/skins/default/templates/header.jinja.html56
-rw-r--r--askbot/skins/default/templates/input_bar.html1
-rw-r--r--askbot/skins/default/templates/input_bar.jinja.html45
-rw-r--r--askbot/skins/default/templates/macros.html80
-rw-r--r--askbot/skins/default/templates/questions.jinja.html305
-rw-r--r--askbot/skins/default/templates/tag_selector.jinja.html41
-rw-r--r--askbot/skins/loaders.py33
-rw-r--r--askbot/templatetags/extra_filters.py18
-rw-r--r--askbot/templatetags/extra_tags.py94
-rw-r--r--askbot/utils/functions.py77
-rw-r--r--askbot/views/readers.py14
-rw-r--r--coffin/.___init__.pybin0 -> 187 bytes
-rw-r--r--coffin/._common.pybin0 -> 187 bytes
-rw-r--r--coffin/._interop.pybin0 -> 184 bytes
-rw-r--r--coffin/__init__.py44
-rw-r--r--coffin/common.py148
-rw-r--r--coffin/conf/.___init__.pybin0 -> 184 bytes
-rw-r--r--coffin/conf/__init__.py0
-rw-r--r--coffin/conf/urls/__init__.py0
-rw-r--r--coffin/conf/urls/defaults.py4
-rw-r--r--coffin/contrib/__init__.py0
-rw-r--r--coffin/contrib/markup/__init__.py0
-rw-r--r--coffin/contrib/markup/models.py0
-rw-r--r--coffin/contrib/markup/templatetags/__init__.py0
-rw-r--r--coffin/contrib/markup/templatetags/markup.py15
-rw-r--r--coffin/contrib/syndication/__init__.py0
-rw-r--r--coffin/contrib/syndication/feeds.py36
-rw-r--r--coffin/interop.py120
-rw-r--r--coffin/shortcuts/.___init__.pybin0 -> 187 bytes
-rw-r--r--coffin/shortcuts/__init__.py25
-rw-r--r--coffin/template/.___init__.pybin0 -> 187 bytes
-rw-r--r--coffin/template/._defaultfilters.pybin0 -> 185 bytes
-rw-r--r--coffin/template/._defaulttags.pybin0 -> 184 bytes
-rw-r--r--coffin/template/._library.pybin0 -> 187 bytes
-rw-r--r--coffin/template/._loader.pybin0 -> 187 bytes
-rw-r--r--coffin/template/._loaders.pybin0 -> 184 bytes
-rw-r--r--coffin/template/__init__.py93
-rw-r--r--coffin/template/defaultfilters.py99
-rw-r--r--coffin/template/defaulttags.py364
-rw-r--r--coffin/template/library.py215
-rw-r--r--coffin/template/loader.py66
-rw-r--r--coffin/template/loaders.py38
-rw-r--r--coffin/views/__init__.py0
-rw-r--r--coffin/views/defaults.py35
-rw-r--r--coffin/views/generic/._simple.pybin0 -> 185 bytes
-rw-r--r--coffin/views/generic/__init__.py1
-rw-r--r--coffin/views/generic/simple.py6
51 files changed, 2263 insertions, 86 deletions
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py
index 69129813..6e84bd42 100644
--- a/askbot/models/__init__.py
+++ b/askbot/models/__init__.py
@@ -1180,6 +1180,50 @@ def get_profile_link(self):
return mark_safe(profile_link)
+def user_get_karma_summary(self):
+ """returns human readable sentence about
+ status of user's karma"""
+ return _("%(username)s karma is %(reputation)s") % \
+ {'username': self.username, 'reputation': self.reputation}
+
+def user_get_badge_summary(self):
+ """returns human readable sentence about
+ number of badges of different levels earned
+ by the user. It is assumed that user has some badges"""
+ badge_bits = list()
+ if self.gold:
+ bit = ungettext(
+ 'one gold badge',
+ '%(count)d gold badges',
+ self.gold
+ ) % {'count': self.gold}
+ badge_bits.append(bit)
+ if self.silver:
+ bit = ungettext(
+ 'one silver badge',
+ '%(count)d silver badges',
+ self.gold
+ ) % {'count': self.silver}
+ badge_bits.append(bit)
+ if self.silver:
+ bit = ungettext(
+ 'one bronze badge',
+ '%(count)d bronze badges',
+ self.gold
+ ) % {'count': self.bronze}
+ badge_bits.append(bit)
+
+ if len(badge_bits) == 1:
+ badge_str = badge_bits[0]
+ elif len(badge_bits) > 1:
+ last_bit = badge_bits.pop()
+ badge_str = ', '.join(badge_bits)
+ badge_str = _('%(item1)s and %(item2)s') % \
+ {'item1': badge_str, 'item2': last_bit}
+ else:
+ raise ValueError('user must have badges to call this function')
+ return _("%(user)s has %(badges)s") % {'user': self.username, 'badges':badge_str}
+
#series of methods for user vote-type commands
#same call signature func(self, post, timestamp=None, cancel=None)
#note that none of these have business logic checks internally
diff --git a/askbot/skins/default/templates/ask_form.jinja.html b/askbot/skins/default/templates/ask_form.jinja.html
new file mode 100644
index 00000000..a7f61304
--- /dev/null
+++ b/askbot/skins/default/templates/ask_form.jinja.html
@@ -0,0 +1,62 @@
+<div id="askform">
+ <form id="fmask" action="" method="post" >
+ <div class="form-item">
+ {% comment %}
+ <label for="id_title" ><strong>{{ form.title.label_tag }}:</strong></label>
+ {% endcomment %}
+ <div id="askFormBar">
+ {% if not request.user.is_authenticated() %}
+ <p>{% trans %}login to post question info{% endtrans %}</p>
+ {% else %}
+ {% if settings.EMAIL_VALIDATION %}
+ {% if not request.user.email_isvalid %}
+ {% trans email=request.user.email %}must have valid {{email}} to post,
+ see {{email_validation_faq_url}}
+ {% endtrans %}
+ {% endif %}
+ {% endif %}
+ {% endif %}
+ <input id="id_title" class="questionTitleInput" name="title"
+ value="{% if form.initial.title %}{{form.initial.title}}{% endif %}"/>
+ </div>
+ {{ form.title.errors }}
+ <span class="form-error"></span><br/>
+ <div class="title-desc">
+ {{ form.title.help_text }}
+ </div>
+ </div>
+ <div class="form-item">
+ <div id="wmd-button-bar" class="wmd-panel"></div>
+ {{ form.text }}
+ <div class="preview-toggle">
+ <table>
+ <tr>
+ <td>
+ <span id="pre-collapse" title="{% trans %}Toggle the real time Markdown editor preview{% endtrans %}">{% trans "toggle preview{% endtrans %}</span>
+ </td>
+ {% if settings.WIKI_ON %}
+ <td style="text-align:right;">
+ {{ form.wiki }} <span style="font-weight:normal;cursor:help" title="{{form.wiki.help_text}}">{{ form.wiki.label_tag }} </span>
+ </td>
+ {% endif %}
+ </tr>
+
+ </table>
+ </div>
+ <div id="previewer" class="wmd-preview"></div>
+ <span class="form-error"></span>
+ </div>
+ <div class="form-item">
+ <strong>{{ form.tags.label_tag }}:</strong> {% trans %}(required){% endtrans %} <span class="form-error"></span><br/>
+ {{ form.tags }} {{ form.tags.errors }}
+ </div>
+ <p class="title-desc">
+ {{ form.tags.help_text }}
+ </p>
+ {% if not request.user.is_authenticated() %}
+ <input type="submit" value="{% trans %}Login/signup to post your question{% endtrans %}" class="submit" />
+ {% else %}
+ <input type="submit" value="{% trans %}Ask your question{% endtrans %}" class="submit" />
+ {% endif %}
+ </form>
+</div>
diff --git a/askbot/skins/default/templates/base.jinja.html b/askbot/skins/default/templates/base.jinja.html
new file mode 100644
index 00000000..3c28fb45
--- /dev/null
+++ b/askbot/skins/default/templates/base.jinja.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<!-- template base.jinja.html -->
+{% load extra_filters %}
+{% load extra_tags %}
+{% load smart_if %}
+{% load i18n %}
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>{% block title %}{% endblock %} - {{ settings.APP_TITLE }}</title>
+ {% spaceless %}
+ {% block meta %}{% endblock %}
+ {% block meta_description %}
+ <meta name="description" content="{{settings.APP_DESCRIPTION}}" />
+ {% endblock %}
+ {% endspaceless %}
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+ <meta name="keywords" content="{%block keywords%}{%endblock%},{{settings.APP_KEYWORDS}}" />
+ {% if settings.GOOGLE_SITEMAP_CODE %}
+ <meta name="google-site-verification" content="{{settings.GOOGLE_SITEMAP_CODE}}" />
+ {% endif %}
+ <link rel="shortcut icon" href="{{ "/images/favicon.gif"|media }}" />
+ <link href="{{"/style/style.css"|media }}" rel="stylesheet" type="text/css" />
+ <script src="http://www.google.com/jsapi" type="text/javascript"></script>
+ <script type="text/javascript">google.load("jquery", "1.2.6");</script>
+ <script type="text/javascript">
+ var i18nLang = '{{settings.LANGUAGE_CODE}}';
+ var scriptUrl = '/{{settings.ASKBOT_URL}}'
+ var askbotSkin = '{{settings.ASKBOT_DEFAULT_SKIN}}';
+ {% if settings.ENABLE_MATHJAX %}
+ var enableMathJax = true;
+ {% else %}
+ var enableMathJax = false;
+ {% endif %}
+ </script>
+ <script type='text/javascript' src='{{"/js/com.cnprog.i18n.js"|media }}'></script>
+ <script type='text/javascript' src='{{"/js/jquery.i18n.js"|media }}'></script>
+ <script type='text/javascript' src='{{"/js/com.cnprog.utils.js"|media }}'></script>
+ {% if settings.ENABLE_MATHJAX %}
+ <script type='text/javascript' src='{{settings.MATHJAX_BASE_URL}}/MathJax.js'>
+ MathJax.Hub.Config({
+ extensions: ["tex2jax.js"],
+ jax: ["input/TeX","output/HTML-CSS"],
+ tex2jax: {inlineMath: [["$","$"],["\\(","\\)"]]}
+ });
+ </script>
+ {% endif %}
+ {% if user_messages %}
+ <style type="text/css">
+ body { margin-top:2.4em; }
+ </style>
+ <script type="text/javascript">
+ $(document).ready(function() {
+ $('#validate_email_alert').click(function(){notify.close(true)})
+ notify.show();
+ });
+ </script>
+ {% endif %}
+ {% if active_tab != "tags" and active_tab != "users" %}
+ {% comment %}start asking question with title from search query{% endcomment %}
+ <script type="text/javascript">
+ $(document).ready(function(){
+ $('#nav_ask').click(
+ function(){
+ var starting_title = $('#keywords').attr('value');
+ var new_url = $(this).attr('href') + '?title=' + starting_title;
+ window.location.href = new_url;
+ return false;
+ }
+ );
+ });
+ </script>
+ {% comment %}focus input on the search bar{% endcomment %}
+ <script type="text/javascript">
+ $(document).ready(function() {
+ {% if active_tab != "ask" %}
+ $('#keywords').focus();
+ {% else %}
+ $('#id_title').focus();
+ {% endif %}
+ });
+ </script>
+ {% endif %}
+ {% block forejs %}
+ {% endblock %}
+ </head>
+ {% if page_class %}
+ <body class="{{page_class}}">
+ {% else %}
+ <body>
+ {% endif %}
+ <div class="notify" style="display:none">
+ {% autoescape off %}
+ {% if user_messages %}
+ {% for message in user_messages %}
+ <p class="darkred">{{ message }}</p>
+ {% endfor %}
+ {% endif %}
+ {% endautoescape %}
+ <a id="close-notify" onclick="notify.close(true)">&times;</a>
+ </div>
+ {% include "header.jinja.html" %}
+ <div id="wrapper">
+ <div id="room">
+ <div id="CALeft">
+ {% include "input_bar.jinja.html" %}
+ {% block content%}
+ {% endblock%}
+
+ </div>
+ <div id="CARight">
+ {% block sidebar%}
+ {% endblock%}
+ </div>
+ <div id="tail" style="clear:both;">
+ {% block tail %}
+ {% endblock %}
+ </div>
+ </div>
+ <div class="spacer3"></div>
+ </div>
+ {% include "footer.html" %}
+ {% block endjs %}
+ {% endblock %}
+ </body>
+</html>
+<!-- end template base.jinja.html -->
diff --git a/askbot/skins/default/templates/footer.jinja.html b/askbot/skins/default/templates/footer.jinja.html
new file mode 100644
index 00000000..9b6130f8
--- /dev/null
+++ b/askbot/skins/default/templates/footer.jinja.html
@@ -0,0 +1,44 @@
+<!-- template footer.jinja.html -->
+<div id="ground">
+ <div>
+ <div class="footerLinks" >
+ <a href="{% url about %}">{% trans %}about{% endtrans %}</a><span class="link-separator"> |</span>
+ <a href="{% url faq %}">{% trans %}faq{% endtrans %}</a><span class="link-separator"> |</span>
+ <a href="{% url privacy %}">{% trans %}privacy policy{% endtrans %}</a><span class="link-separator"> |</span>
+ {% spaceless %}
+ <a href=
+ {% if settings.FEEDBACK_SITE_URL %}
+ "{{settings.FEEDBACK_SITE_URL}}"
+ target="_blank">
+ {% else %}
+ "{% url feedback %}?next={{request.path}}">
+ {% endif %}
+ {% trans %}give feedback{% endtrans %}
+ </a>
+ {% endspaceless %}
+ </div>
+ <p>
+ <a href="http://askbot.org" target="_blank">
+ powered by ASKBOT
+ </a><br/>{{settings.APP_COPYRIGHT}}
+ </p>
+ </div>
+ <div id="licenseLogo">
+ <a href="http://creativecommons.org/licenses/by/3.0/">
+ <img src="{% media "/images/cc-wiki.png" %}" title="Creative Commons: Attribution - Share Alike" alt="cc-wiki" width="50" height="68" />
+ </a>
+ </div>
+</div>
+{% if settings.GOOGLE_ANALYTICS_KEY %}
+<script type="text/javascript">
+ var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
+ document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
+ </script>
+ <script type="text/javascript">
+ try {
+ var pageTracker = _gat._getTracker('{{ settings.GOOGLE_ANALYTICS_KEY }}');
+ pageTracker._trackPageview();
+ } catch(err) {}
+</script>
+{% endif %}
+<!-- end template footer.jinja.html -->
diff --git a/askbot/skins/default/templates/header.jinja.html b/askbot/skins/default/templates/header.jinja.html
new file mode 100644
index 00000000..2152fea9
--- /dev/null
+++ b/askbot/skins/default/templates/header.jinja.html
@@ -0,0 +1,56 @@
+<!-- template header.jinja.html -->
+{% import "macros.html" %}
+<div id="roof">
+ <div id="navBar">
+ <div id="top">
+ {% if request.user.is_authenticated() %}
+ <a href="{{ request.user.get_absolute_url() }}">{{ request.user.username }}</a>
+ {% spaceless %}
+ <a class='ab-responses-envelope' href="{{request.user.get_absolute_url()}}?sort=responses">
+ <img
+ alt="{% trans username=request.user.username %}responses for {{username}}{% endtrans %}"
+ {% if request.user.response_count > 0 %}
+ src="{{ "/images/mail-envelope-full.png"|media }}"
+ title="{% trans response_count=request.user.response %}you have a new response{% pluralize %}you nave {{response_count}} new responses{% endtrans %}"
+ {% else %}
+ src="{{ "/images/mail-envelope-empty.png"|media }}"
+ title="{% trans %}no new responses yet{% endtrans %}"
+ {% endif %}
+ />
+ </a>
+ {% endspaceless %}
+ ({{ macros.user_long_score_and_badge_summary(user) }})
+ <a href="{% url logout %}">{% trans %}logout{% endtrans %}</a>
+ {% else %}
+ <a href="{% url user_signin %}">{% trans %}login{% endtrans %}</a>
+ {% endif %}
+ <a href="{% url about %}">{% trans %}about{% endtrans %}</a>
+ <a href="{% url faq %}">{% trans %}faq{% endtrans %}</a>
+ {% if request.user.is_administrator %}
+ <a href="{% url site_settings %}">{% trans %}settings{% endtrans %}</a>
+ {% endif %}
+ </div>
+ <table border="0" cellspacing="0" cellpadding="0">
+ <tr>
+ <td id="logoContainer">
+ <div id="logo">
+ <a href="{% url questions %}?start_over=true"><img
+ src="{% media settings.SITE_LOGO_URL %}"
+ title="{% trans %}back to home page{% endtrans %}"
+ alt="{% trans site=settings.APP_SHORT_NAME %}{{site}} logo{% endtrans %}"/></a>
+ </div>
+ </td>
+ <td id="navTabContainer" valign="bottom" align="left">
+ <div class="nav">
+ <a id="nav_questions" href="{% url questions %}" >{% trans %}questions{% endtrans %}</a>
+ <a id="nav_tags" href="{% url tags %}">{% trans %}tags{% endtrans %}</a>
+ <a id="nav_users" href="{% url users %}">{% trans %}users{% endtrans %}</a>
+ <a id="nav_badges" href="{% url badges %}">{% trans %}badges{% endtrans %}</a>
+ <a id="nav_ask" href="{% url ask %}" class="special">{% trans %}ask a question{% endtrans %}</a>
+ </div>
+ </td>
+ </tr>
+ </table>
+ </div>
+</div>
+<!-- end template header.html -->
diff --git a/askbot/skins/default/templates/input_bar.html b/askbot/skins/default/templates/input_bar.html
index 018f79d9..1ba11848 100644
--- a/askbot/skins/default/templates/input_bar.html
+++ b/askbot/skins/default/templates/input_bar.html
@@ -19,6 +19,7 @@
class="searchInput"
{% endif %}
type="text"
+ autocomplete="off"
value="{{ query|default_if_none:"" }}"
name="query"
id="keywords"/>
diff --git a/askbot/skins/default/templates/input_bar.jinja.html b/askbot/skins/default/templates/input_bar.jinja.html
new file mode 100644
index 00000000..886707ff
--- /dev/null
+++ b/askbot/skins/default/templates/input_bar.jinja.html
@@ -0,0 +1,45 @@
+{% if active_tab != "ask" %}
+{% spaceless %}
+<div id="searchBar">
+ {% comment %}url action depends on which tab is active{% endcomment %}
+ <form
+ {% if active_tab == "tags" or active_tab == "users" %}
+ action="{% url search %}"
+ {% else %}
+ action="{% url questions %}"
+ {% endif %}
+ method="get">
+ {% comment %} class was searchInput {% endcomment %}
+ <input
+ {% if query %}
+ class="searchInputCancelable"
+ {% else %}
+ class="searchInput"
+ {% endif %}
+ type="text"
+ autocomplete="off"
+ value="{{ query|default_if_none('') }}"
+ name="query"
+ id="keywords"/>
+ {% if query %}{% comment %}query is only defined by questions view{% endcomment %}
+ <input type="button"
+ value="x"
+ name="reset_query"
+ {% comment %}todo - make sure it works on Enter keypress{% endcomment %}
+ onclick="window.location.href='{% url questions %}?reset_query=true'"
+ class="cancelSearchBtn"/>
+ {% endif %}
+ <input type="submit" value="{% trans "search" %}" name="search" class="searchBtn" />
+ {% if active_tab == "tags" %}
+ <input type="hidden" name="t" value="tag"/>
+ {% else %}
+ {% if active_tab == "users" %}
+ <input type="hidden" name="t" value="user"/>
+ {% endif %}
+ {% endif %}
+ </form>
+</div>
+{% endspaceless %}
+{% else %}
+ {% include "ask_form.jinja.html" %}
+{% endif %}
diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html
new file mode 100644
index 00000000..8ef9049b
--- /dev/null
+++ b/askbot/skins/default/templates/macros.html
@@ -0,0 +1,80 @@
+{%- macro user_score_and_badge_summary(user) -%}
+ <span class="reputation-score"
+ title="{{user.get_karma_summary}}"
+ >{{user.reputation}}</span>
+ {% if user.gold or user.silver or user.bronze %}
+ <span title="{{user.get_badge_summary}}">
+ {% if user.gold %}
+ <span class='badge1'>&#9679;</span>
+ <span class="badgecount">{{user.gold}}</span>
+ {% endif %}
+ {% if user.silver %}
+ <span class='badge2'>&#9679;</span>
+ <span class="badgecount">{{user.silver}}</span>
+ {% endif %}
+ {% if user.bronze %}
+ <span class='badge3'>&#9679;</span>
+ <span class="badgecount">{{user.bronze}}</span>
+ {% endif %}
+ </span>
+ {% endif %}
+{%- endmacro -%}
+
+{%- macro user_long_score_and_badge_summary(user) -%}
+ <span class="reputation-score"
+ title="{{user.get_karma_summary}}"
+ >{% trans %}karma:{% endtrans %} {{user.reputation}}</span>
+ {% if user.gold or user.silver or user.bronze %}
+ <span title="{{user.get_badge_summary}}">{% trans %}badges:{% endtrans %}
+ {% if user.gold %}
+ <span class='badge1'>&#9679;</span>
+ <span class="badgecount">{{user.gold}}</span>
+ {% endif %}
+ {% if user.silver %}
+ <span class='badge2'>&#9679;</span>
+ <span class="badgecount">{{user.silver}}</span>
+ {% endif %}
+ {% if user.bronze %}
+ <span class='badge3'>&#9679;</span>
+ <span class="badgecount">{{user.bronze}}</span>
+ {% endif %}
+ </span>
+ {% endif %}
+{%- endmacro -%}
+
+{%- macro paginator(p) -%}{# p is paginator context dictionary #}
+{% spaceless %}
+ {% if p.is_paginated %}
+ <div class="paginator">
+ {% if p.has_previous %}
+ <span class="prev"><a href="{{p.base_url}}page={{ p.contextprevious }}{{ p.extend_url }}" title="{% trans %}previous{% endtrans %}">
+ &laquo; {% trans %}previous{% endtrans %}</a></span>
+ {% endif %}
+ {% if not p.in_leading_range %}
+ {% for num in p.pages_outside_trailing_range %}
+ <span class="page"><a href="{{p.base_url}}page={{ num }}{{ p.extend_url }}" >{{ num }}</a></span>
+ {% endfor %}
+ ...
+ {% endif %}
+
+ {% for num in p.page_numbers %}
+ {% if num == p.page and p.pages != 1%}
+ <span class="curr" title="{% trans %}current page{% endtrans %}">{{ num }}</span>
+ {% else %}
+ <span class="page"><a href="{{p.base_url}}page={{ num }}{{ p.extend_url }}" title="{% trans %}page number {{num}}{% endtrans %}">{{ num }}</a></span>
+ {% endif %}
+ {% endfor %}
+
+ {% if not p.in_trailing_range %}
+ ...
+ {% for num in p.pages_outside_leading_range|reverse %}
+ <span class="page"><a href="{{p.base_url}}page={{ num }}{{ p.extend_url }}" title="{% trans %}page number {{ num }}{% endtrans %}">{{ num }}</a></span>
+ {% endfor %}
+ {% endif %}
+ {% if p.has_next %}
+ <span class="next"><a href="{{p.base_url}}page={{ next }}{{ p.extend_url }}" title="{% trans %}next page{% endtrans %}">{% trans %}next page{% endtrans %} &raquo;</a></span>
+ {% endif %}
+ </div>
+ {% endif %}
+{% endspaceless %}
+{%- endmacro -%}
diff --git a/askbot/skins/default/templates/questions.jinja.html b/askbot/skins/default/templates/questions.jinja.html
new file mode 100644
index 00000000..9e0ce324
--- /dev/null
+++ b/askbot/skins/default/templates/questions.jinja.html
@@ -0,0 +1,305 @@
+{% extends "base.jinja.html" %}
+<!-- questions.html -->
+{% import "macros.html" as macros %}
+{% block title %}{% spaceless %}{% trans %}Questions{% endtrans %}{% endspaceless %}{% endblock %}
+{% block forejs %}
+ <script type="text/javascript">
+ var tags = {{ tags_autocomplete|safe }};
+ $().ready(function(){
+ var sort_tab_id = "{{ sort }}";
+ $("#"+sort_tab_id).attr('className',"on");
+ var scope_tab_id = "{{ scope }}";
+ $("#"+scope_tab_id).attr('className',"on");
+ var on_tab = '#nav_questions';
+ $(on_tab).attr('className','on');
+ Hilite.exact = false;
+ Hilite.elementid = "listA";
+ Hilite.debug_referrer = location.href;
+ });
+ </script>
+ <script type='text/javascript' src='{{"/js/com.cnprog.editor.js"|media}}'></script>
+ <script type='text/javascript' src='{{"/js/com.cnprog.tag_selector.js"|media}}'></script>
+{% endblock %}
+{% block content %}
+{% cache 600 "scope_sort_tabs" search_tags request.user scope sort query context.page context.page_size current_language %}
+<div class="tabBar">
+ <div class="tabsC">
+ <span class="label">{% trans %}In:{% endtrans %}</span>
+ <a id="all" class="off" href="?scope=all" title="{% trans %}see all questions{% endtrans %}">{% trans %}all{% endtrans %}</a>
+ <a id="unanswered" class="off" href="?scope=unanswered&amp;sort=coldest" title="{% trans %}see unanswered questions{% endtrans %}">{% trans %}unanswered{% endtrans %}</a>
+ {% if request.user.is_authenticated() %}
+ <a id="favorite" class="off" href="?scope=favorite" title="{% trans %}see your favorite questions{% endtrans %}">{% trans %}favorite{% endtrans %}</a>
+ {% endif %}
+ </div>
+ <div class="tabsA">
+ <span class="label">{% trans %}Sort by:{% endtrans %}</span>
+ {% if sort == "oldest" %}
+ <a id="oldest"
+ href="?sort=latest"
+ class="off"
+ title="{% trans %}click to see the newest questions{% endtrans %}">{% trans %}oldest{% endtrans %}</a>
+ {% elif sort == "latest" %}
+ <a id="latest"
+ href="?sort=oldest"
+ class="off"
+ title="{% trans %}click to see the oldest questions{% endtrans %}">{% trans %}newest{% endtrans %}</a>
+ {% else %}
+ <a id="latest"
+ href="?sort=latest"
+ class="off"
+ title="{% trans %}click to see the newest questions{% endtrans %}">{% trans %}newest{% endtrans %}</a>
+ {% endif %}
+
+ {% if sort == "inactive" %}
+ <a id="inactive"
+ href="?sort=active"
+ class="off"
+ title="{% trans %}click to see the most recently updated questions{% endtrans %}">{% trans %}inactive{% endtrans %}</a>
+ {% elif sort == "active" %}
+ <a id="active"
+ href="?sort=inactive"
+ class="off"
+ title="{% trans %}click to see the least recently updated questions{% endtrans %}">{% trans %}active{% endtrans %}</a>
+ {% else %}
+ <a id="active"
+ href="?sort=active"
+ class="off"
+ title="{% trans %}click to see the most recently updated questions{% endtrans %}">{% trans %}active{% endtrans %}</a>
+ {% endif %}
+
+ {% if sort == "coldest" %}
+ <a id="coldest"
+ href="?sort=hottest"
+ class="off"
+ title="{% trans %}click to see hottest questions{% endtrans %}">{% trans %}less answers{% endtrans %}</a>
+ {% elif sort == "hottest" %}
+ <a id="hottest"
+ href="?sort=coldest"
+ class="off"
+ title="{% trans %}click to see coldest questions{% endtrans %}">{% trans %}more answers{% endtrans %}</a>
+ {% else %}
+ <a id="hottest"
+ href="?sort=hottest"
+ class="off"
+ title="{% trans %}click to see hottest questions{% endtrans %}">{% trans %}more answers{% endtrans %}</a>
+ {% endif %}
+
+ {% if sort == "leastvoted" %}
+ <a id="leastvoted"
+ href="?sort=mostvoted"
+ class="off"
+ title="{% trans %}click to see most voted questions{% endtrans %}">{% trans %}unpopular{% endtrans %}</a>
+ {% elif sort == "mostvoted" %}
+ <a id="mostvoted"
+ href="?sort=leastvoted"
+ class="off"
+ title="{% trans %}click to see least voted questions{% endtrans %}">{% trans %}popular{% endtrans %}</a>
+ {% else %}
+ <a id="mostvoted"
+ href="?sort=mostvoted"
+ class="off"
+ title="{% trans %}click to see most voted questions{% endtrans %}">{% trans %}popular{% endtrans %}</a>
+ {% endif %}
+ </div>
+</div>
+{% endcache %}
+{% if questions_count > 0 %}
+ <div style="clear:both">
+ <p style="float:right;margin:3px 3px 0 0;">
+ (<a style="text-decoration:none;"
+ href="{{settings.APP_URL}}/feeds/rss/"
+ title="{% trans %}subscribe to the questions feed{% endtrans %}"
+ ><img
+ style="vertical-align:middle;"
+ alt="{% trans %}subscribe to the questions feed{% endtrans %}"
+ src="{{"/images/feed-icon-small.png"|media}}"/> {% trans %}rss feed{% endtrans %}</a>)
+ </p>
+ <p id="question-count" class="search-result-summary">
+ {% if author_name or search_tags or query %}
+ {% trans cnt=questions_count, q_num=questions_count|intcomma %}
+ {{q_num}} question found
+ {% pluralize %}
+ {{q_num}} questions found
+ {% endtrans %}
+ {% else %}
+ {% trans cnt=questions_count, q_num=questions_count|intcomma %}{{q_num}} question{% pluralize %}{{q_num}} questions{% endtrans %}
+ {% endif %}
+ {% if author_name %}
+ {% trans %}with {{author_name}}'s contributions{% endtrans %}
+ {% endif %}
+ {% if search_tags %}{% if author_name %}, {% endif %}
+ {% trans %}tagged{% endtrans %}
+ "{{ search_tags|join('", "') }}"
+ {% endif %}
+ </p>
+ {% if author_name or search_tags or query %}
+ <p class="search-tips">{% trans %}Search tips:{% endtrans %}
+ {% if reset_method_count > 1 %}
+ {% if author_name %}
+ <a href="{% url questions %}?reset_author=true">{% trans %}reset author{% endtrans %}</a>
+ {% endif %}
+ {% if search_tags %}{% if author_name and query %}, {% elif author_name %}{% trans %} or {% endtrans %}{% endif %}
+ <a href="{% url questions %}?reset_tags=true">{% trans %}reset tags{% endtrans %}</a>
+ {% endif %}
+ {% if query %}{% trans %} or {% endtrans %}
+ <a href="{% url questions %}?start_over=true">{% trans %}start over{% endtrans %}</a>
+ {% endif %}
+ {% else %}
+ <a href="{% url questions %}?start_over=true">{% trans %}start over{% endtrans %}</a>
+ {% endif %}
+ {% trans %} - to expand, or dig in by adding more tags and revising the query.{% endtrans %}
+ </p>
+ {% else %}
+ <p class="search-tips">{% trans %}Search tip:{% endtrans %} {% trans %}add tags and a query to focus your search{% endtrans %}</p>
+ {% endif %}
+ </div>
+{% endif %}
+<div id="listA">
+{% cache 60 "questions" questions search_tags scope sort query context.page context.page_size language_code %}
+ {% for question in questions.object_list %}
+ <div class="short-summary">
+ <div class="counts">
+ <div class="votes">
+ <span
+ class="item-count"
+ {% if question.score == 0 %}
+ style="background:{{settings.COLORS_VOTE_COUNTER_EMPTY_BG}};color:{{settings.COLORS_VOTE_COUNTER_EMPTY_FG}}"
+ {% else %}
+ style="background:{{settings.COLORS_VOTE_COUNTER_MIN_BG}};color:{{settings.COLORS_VOTE_COUNTER_MIN_FG}}"
+ {% endif %}
+ >{{question.score|humanize_counter}}</span>
+ <div>
+ {% trans cnt=question.score %}vote{% pluralize %}votes{% endtrans %}
+ </div>
+ </div >
+ <div class="votes">
+ <span
+ class="item-count"
+ {% if question.answer_count == 0 %}
+ style="background:{{settings.COLORS_ANSWER_COUNTER_EMPTY_BG}};color:{{settings.COLORS_ANSWER_COUNTER_EMPTY_FG}}"
+ {% else %}
+ {% if question.answer_accepted %}
+ style="background:{{settings.COLORS_ANSWER_COUNTER_ACCEPTED_BG}};color:{{settings.COLORS_ANSWER_COUNTER_ACCEPTED_FG}}"
+ {% else %}
+ style="background:{{settings.COLORS_ANSWER_COUNTER_MIN_BG}};color:{{settings.COLORS_ANSWER_COUNTER_MIN_FG}}"
+ {% endif %}
+ {% endif %}
+ >{{question.answer_count|humanize_counter}}</span>
+ <div>
+ {% trans cnt=question.answer_count %}answer{% pluralize %}answers{% endtrans %}
+ </div>
+ </div>
+ <div class="votes">
+ <span class="item-count"
+ {% if question.view_count == 0 %}
+ style="background:{{settings.COLORS_VIEW_COUNTER_EMPTY_BG}};color:{{settings.COLORS_VIEW_COUNTER_EMPTY_FG}}"
+ {% else %}
+ style="background:{{settings.COLORS_VIEW_COUNTER_MIN_BG}};color:{{settings.COLORS_VIEW_COUNTER_MIN_FG}}"
+ {% endif %}
+ >{{question.view_count|humanize_counter}}</span>
+ <div>
+ {% trans cnt=question.view_count %}view{% pluralize %}views{% endtrans %}
+ </div>
+ </div>
+ </div>
+ <h2><a title="{{question.summary}}" href="{{ question.get_absolute_url() }}">{{question.title}}</a></h2>
+ <div class="userinfo">
+ <span class="relativetime" title="{{question.last_activity_at}}">{{ question.last_activity_at|diff_date }}</span>
+ {{question.last_activity_by.get_profile_link()}}
+ {{macros.user_score_and_badge_summary(question.last_activity_by)}}
+ </div>
+ <div class="tags">
+ {% for tag in question.tagname_list %}
+ <a href="{% url questions %}?tags={{tag|urlencode}}" title="{% trans %}see questions tagged '{{ tag }}'{% endtrans %}" rel="tag">{{ tag }}</a>
+ {% endfor %}
+ </div>
+ </div>
+ {% endfor %}
+{% endcache %}
+ {# comment todo: fix css here #}
+ {% if questions_count == 0 %}
+ {# todo: add tips to widen selection #}
+ <p class="evenMore" style="padding-top:30px;text-align:center;">
+ {% if scope == "unanswered" %}
+ {% trans %}There are no unanswered questions here{% endtrans %}
+ {% endif %}
+ {% if scope == "favorite" %}
+ {% trans %}No favorite questions here. {% endtrans %}
+ {% trans %}Please start (bookmark) some questions when you visit them{% endtrans %}
+ {% endif %}
+ </p>
+ {% if query or search_tags or author_name %}
+ <p class="evenMore" style="text-align:center">
+ {% trans %}You can expand your search by {% endtrans %}
+ {% if reset_method_count > 1 %}
+ {% if author_name %}
+ <a href="{% url questions %}?reset_author=true">{% trans %}resetting author{% endtrans %}</a>
+ {% endif %}
+ {% if search_tags %}{% if author_name and query %}, {% elif author_name %}{% trans %} or {% endtrans %}{% endif %}
+ <a href="{% url questions %}?reset_tags=true">{% trans %}resetting tags{% endtrans %}</a>
+ {% endif %}
+ {% if query %}{% trans %} or {% endtrans %}
+ <a href="{% url questions %}?start_over=true">{% trans %}starting over{% endtrans %}</a>
+ {% endif %}
+ {% else %}
+ <a href="{% url questions %}?start_over=true">{% trans %}starting over{% endtrans %}</a>
+ {% endif %}
+ </p>
+ {% endif %}
+ <p class="evenMore" style="text-align:center">
+ <a href="{% url ask %}">{% trans %}Please always feel free to ask your question!{% endtrans %}</a>
+ </p>
+ {% else %}
+ <p class="evenMore" style="padding-left:9px">
+ {% trans %}Did not find what you were looking for?{% endtrans %}
+ <a href="{% url ask %}">{% trans %}Please, post your question!{% endtrans %}</a>
+ </p>
+ {% endif %}
+</div>
+<script type="text/javascript" src="{{"/js/live_search.js"|media}}"></script>
+{% endblock %}
+
+ {% block tail %}
+ {% if questions_count > 10 %}{# todo: remove magic number #}
+ <div id="pager" class="pager">{{ macros.paginator(context|setup_paginator) }}</div>
+ <div class="pagesize">{{ macros.page_size_switch(context) }}</div>
+ {% endif %}
+ {% endblock %}
+
+{% block sidebar %}
+ {% if contributors %}
+ {% cache 600 "contributors" contributors search_tags scope sort query context.page context.page_size language_code %}
+ <div id="contrib-users" class="boxC">
+ <h3 class="subtitle">{% trans %}Contributors{% endtrans %}</h3>
+ {% spaceless %}
+ {% for person in contributors %}
+ {% gravatar person 48 %}
+ {% endfor %}
+ {% endspaceless %}
+ </div>
+ {% endcache %}
+ {% endif %}
+
+ {% if request.user.is_authenticated() %}
+ {% include "tag_selector.jinja.html" %}
+ {% endif %}
+
+ {% if tags %}
+ {% cache 600 "tags" tags search_tags scope sort query context.page context.page_size language_code %}
+ <div class="boxC">
+ <h3 class="subtitle">{% trans %}Related tags{% endtrans %}</h3>
+ <div id="related-tags" class="tags">
+ {% for tag in tags %}
+ <a
+ rel="tag"
+ title="{% trans tag_name=tag.name %}see questions tagged '{{ tag_name }}'{% endtrans %}"
+ href="{% url questions %}?tags={{tag.name|urlencode}}">{{ tag.name }}</a>
+ <span class="tag-number">&#215; {{ tag.used_count|intcomma }}</span>
+ <br />
+ {% endfor %}
+ </div>
+ </div>
+ {% endcache %}
+ {% endif %}
+{% endblock %}
+<!-- end questions.html -->
diff --git a/askbot/skins/default/templates/tag_selector.jinja.html b/askbot/skins/default/templates/tag_selector.jinja.html
new file mode 100644
index 00000000..ad0942f8
--- /dev/null
+++ b/askbot/skins/default/templates/tag_selector.jinja.html
@@ -0,0 +1,41 @@
+{% comment %}todo - maybe disable navigation from ignored tags here when "hide" is on - with js?{%endcomment%}
+<div id="tagSelector" class="boxC">
+ <h3 class="subtitle">{% trans %}Interesting tags{% endtrans %}</h3>
+ <div class="tags interesting marked-tags">
+ {% for tag_name in interesting_tag_names %}
+ {% spaceless %}
+ <span class="deletable-tag" id="interesting-tag-{{tag_name}}">
+ <a rel="tag"
+ title="{% trans %}see questions tagged '{{ tag_name }}'{% endtrans %}"
+ href="{% url questions %}?tags={{tag_name|urlencode}}">{{tag_name}}</a>
+ <img class="delete-icon"
+ src="{{'/images/close-small-dark.png'|media}}"
+ title="{% trans %}remove '{{tag_name}}' from the list of interesting tags{% endtrans %}"/>
+ </span>
+ {% endspaceless %}
+ {% endfor %}
+ </div>
+ <input id="interestingTagInput" autocomplete="off" type="text"/>&nbsp;
+ <input id="interestingTagAdd" type="submit" value="{% trans %}Add{% endtrans %}"/>
+ <h3 class="subtitle">{% trans %}Ignored tags{% endtrans %}</h3>
+ <div class="tags ignored marked-tags">
+ {% for tag_name in ignored_tag_names %}
+ {% spaceless %}
+ <span class="deletable-tag" id="ignored-tag-{{tag_name}}">
+ <a rel="tag"
+ title="{% trans %}see questions tagged '{{ tag_name }}'{% endbtrans %}"
+ href="{% url questions %}?tags={{tag_name|urlencode}}">{{tag_name}}</a>
+ <img class="delete-icon"
+ src="{{'/images/close-small-dark.png|media{% endtrans %}"
+ title="{% trans %}remove '{{tag_name}}' from the list of ignored tags{% endtrans %}"/>
+ </span>
+ {% endspaceless %}
+ {% endfor %}
+ </div>
+ <input id="ignoredTagInput" autocomplete="off" type="text"/>&nbsp;
+ <input id="ignoredTagAdd" type="submit" value="{% trans %}Add{% endtrans%}"/>
+ <p id="hideIgnoredTagsControl">
+ <input id="hideIgnoredTagsCb" type="checkbox" {% if request.user.hide_ignored_questions %}checked="checked"{% endif %} />&nbsp;
+ <label id="hideIgnoredTagsLabel" for="hideIgnoredTagsCb">{% trans %}keep ignored questions hidden{% endtrans %}</label>
+ <p>
+</div>
diff --git a/askbot/skins/loaders.py b/askbot/skins/loaders.py
index 629ec0fa..59114c5a 100644
--- a/askbot/skins/loaders.py
+++ b/askbot/skins/loaders.py
@@ -1,10 +1,9 @@
-from django.template import loader
from django.template.loaders import filesystem
import os.path
-import os
-import logging
-from askbot.skins import utils
from askbot.conf import settings as askbot_settings
+from django.conf import settings as django_settings
+from coffin.common import CoffinEnvironment
+from jinja2 import loaders as jinja_loaders
#module for skinning askbot
#via ASKBOT_DEFAULT_SKIN configureation variable (not django setting)
@@ -15,6 +14,8 @@ from askbot.conf import settings as askbot_settings
ASKBOT_SKIN_COLLECTION_DIR = os.path.dirname(__file__)
def load_template_source(name, dirs=None):
+ """Django template loader
+ """
if dirs is None:
dirs = (ASKBOT_SKIN_COLLECTION_DIR, )
else:
@@ -28,3 +29,27 @@ def load_template_source(name, dirs=None):
tname = os.path.join('default','templates',name)
return filesystem.load_template_source(tname,dirs)
load_template_source.is_usable = True
+
+class SkinEnvironment(CoffinEnvironment):
+ """Jinja template environment
+ that loads templates from askbot skins
+ """
+
+ def _get_loaders(self):
+ """over-ridden function _get_loaders that creates
+ the loader for the skin templates
+ """
+ loaders = list()
+ skin_name = askbot_settings.ASKBOT_DEFAULT_SKIN
+ skin_dirs = django_settings.TEMPLATE_DIRS + (ASKBOT_SKIN_COLLECTION_DIR,)
+
+ template_dirs = list()
+ for dir in skin_dirs:
+ template_dirs.append(os.path.join(dir, skin_name, 'templates'))
+ for dir in skin_dirs:
+ template_dirs.append(os.path.join(dir, 'default', 'templates'))
+
+ loaders.append(jinja_loaders.FileSystemLoader(template_dirs))
+ return loaders
+
+ENV = SkinEnvironment(extensions=['jinja2.ext.i18n'])
diff --git a/askbot/templatetags/extra_filters.py b/askbot/templatetags/extra_filters.py
index e6339e11..c09812ae 100644
--- a/askbot/templatetags/extra_filters.py
+++ b/askbot/templatetags/extra_filters.py
@@ -5,6 +5,8 @@ from askbot import auth
from askbot import models
from askbot.deps.grapefruit import Color
from askbot.conf import settings as askbot_settings
+from askbot.skins import utils as skin_utils
+from askbot.utils import functions
from django.utils.translation import ugettext as _
import logging
@@ -15,6 +17,22 @@ register = template.Library()
def collapse(input):
return ' '.join(input.split())
+@template.defaultfilters.stringfilter
+@register.filter
+def media(url):
+ """media filter - same as media tag, but
+ to be used as a filter in jinja templates
+ like so {{'/some/url.gif'|media}}
+ """
+ if url:
+ return skin_utils.get_media_url(url)
+ else:
+ return ''
+
+diff_date = register.filter(functions.diff_date)
+
+setup_paginator = register.filter(function.setup_paginator)
+
def make_template_filter_from_permission_assertion(
assertion_name = None,
filter_name = None,
diff --git a/askbot/templatetags/extra_tags.py b/askbot/templatetags/extra_tags.py
index d7730677..685ff102 100644
--- a/askbot/templatetags/extra_tags.py
+++ b/askbot/templatetags/extra_tags.py
@@ -18,7 +18,7 @@ from django.template.defaulttags import url as default_url
from django.core.urlresolvers import reverse
from askbot.skins import utils as skin_utils
from askbot.utils import colors
-from askbot.utils.functions import get_from_dict_or_object
+from askbot.utils import functions
from askbot.utils.slug import slugify
from askbot.templatetags import extra_filters
@@ -42,9 +42,9 @@ def gravatar(user, size):
appropriate values.
"""
#todo: rewrite using get_from_dict_or_object
- gravatar = get_from_dict_or_object(user, 'gravatar')
- username = get_from_dict_or_object(user, 'username')
- user_id = get_from_dict_or_object(user, 'id')
+ gravatar = functions.get_from_dict_or_object(user, 'gravatar')
+ username = functions.get_from_dict_or_object(user, 'username')
+ user_id = functions.get_from_dict_or_object(user, 'id')
slug = slugify(username)
user_profile_url = reverse('user_profile', kwargs={'id':user_id,'slug':slug})
#safe_username = template.defaultfilters.urlencode(username)
@@ -74,54 +74,10 @@ def tag_font_size(max_size, min_size, current_size):
return MIN_FONTSIZE + round((MAX_FONTSIZE - MIN_FONTSIZE) * weight)
-LEADING_PAGE_RANGE_DISPLAYED = TRAILING_PAGE_RANGE_DISPLAYED = 5
-LEADING_PAGE_RANGE = TRAILING_PAGE_RANGE = 4
-NUM_PAGES_OUTSIDE_RANGE = 1
-ADJACENT_PAGES = 2
-@register.inclusion_tag("paginator.html")
-def cnprog_paginator(context):
- """
- custom paginator tag
- Inspired from http://blog.localkinegrinds.com/2007/09/06/digg-style-pagination-in-django/
- """
- if (context["is_paginated"]):
- " Initialize variables "
- in_leading_range = in_trailing_range = False
- pages_outside_leading_range = pages_outside_trailing_range = range(0)
-
- if (context["pages"] <= LEADING_PAGE_RANGE_DISPLAYED):
- in_leading_range = in_trailing_range = True
- page_numbers = [n for n in range(1, context["pages"] + 1) if n > 0 and n <= context["pages"]]
- elif (context["page"] <= LEADING_PAGE_RANGE):
- in_leading_range = True
- page_numbers = [n for n in range(1, LEADING_PAGE_RANGE_DISPLAYED + 1) if n > 0 and n <= context["pages"]]
- pages_outside_leading_range = [n + context["pages"] for n in range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)]
- elif (context["page"] > context["pages"] - TRAILING_PAGE_RANGE):
- in_trailing_range = True
- page_numbers = [n for n in range(context["pages"] - TRAILING_PAGE_RANGE_DISPLAYED + 1, context["pages"] + 1) if n > 0 and n <= context["pages"]]
- pages_outside_trailing_range = [n + 1 for n in range(0, NUM_PAGES_OUTSIDE_RANGE)]
- else:
- page_numbers = [n for n in range(context["page"] - ADJACENT_PAGES, context["page"] + ADJACENT_PAGES + 1) if n > 0 and n <= context["pages"]]
- pages_outside_leading_range = [n + context["pages"] for n in range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)]
- pages_outside_trailing_range = [n + 1 for n in range(0, NUM_PAGES_OUTSIDE_RANGE)]
-
- extend_url = context.get('extend_url', '')
- return {
- "base_url": context["base_url"],
- "is_paginated": context["is_paginated"],
- "previous": context["previous"],
- "has_previous": context["has_previous"],
- "next": context["next"],
- "has_next": context["has_next"],
- "page": context["page"],
- "pages": context["pages"],
- "page_numbers": page_numbers,
- "in_leading_range" : in_leading_range,
- "in_trailing_range" : in_trailing_range,
- "pages_outside_leading_range": pages_outside_leading_range,
- "pages_outside_trailing_range": pages_outside_trailing_range,
- "extend_url" : extend_url
- }
+cnprog_paginator = register.inclusion_tag(
+ functions.setup_paginator
+ "paginator.html",
+ ):
@register.inclusion_tag("pagesize.html")
def cnprog_pagesize(context):
@@ -301,31 +257,7 @@ def convert2tagname_list(question):
question['tagnames'] = [name for name in question['tagnames'].split(u' ')]
return ''
-@register.simple_tag
-def diff_date(date, limen=2, use_on_prefix = False):
- now = datetime.datetime.now()#datetime(*time.localtime()[0:6])#???
- diff = now - date
- days = diff.days
- hours = int(diff.seconds/3600)
- minutes = int(diff.seconds/60)
-
- if days > 2:
- if date.year == now.year:
- date_token = date.strftime("%b %d")
- else:
- date_token = date.strftime("%b %d '%y")
- if use_on_prefix:
- return _('on %(date)s') % { 'date': date_token }
- else:
- return date_token
- elif days == 2:
- return _('2 days ago')
- elif days == 1:
- return _('yesterday')
- elif minutes >= 60:
- return ungettext('%(hr)d hour ago','%(hr)d hours ago',hours) % {'hr':hours}
- else:
- return ungettext('%(min)d min ago','%(min)d mins ago',minutes) % {'min':minutes}
+diff_date = register.simple_tag(functions.diff_date)
@register.simple_tag
def get_latest_changed_timestamp():
@@ -475,10 +407,10 @@ def question_counter_widget(question):
switched from inclusion tag style to in-code template string
for the better speed of the front page rendering
"""
- view_count = get_from_dict_or_object(question, 'view_count')
- answer_count = get_from_dict_or_object(question, 'answer_count')
- vote_count = get_from_dict_or_object(question, 'score')
- answer_accepted = get_from_dict_or_object(question, 'answer_accepted')
+ view_count = functions.get_from_dict_or_object(question, 'view_count')
+ answer_count = functions.get_from_dict_or_object(question, 'answer_count')
+ vote_count = functions.get_from_dict_or_object(question, 'score')
+ answer_accepted = functions.get_from_dict_or_object(question, 'answer_accepted')
#background and foreground colors for each item
(views_fg, views_bg) = colors.get_counter_colors(
diff --git a/askbot/utils/functions.py b/askbot/utils/functions.py
index 7d886a2a..7e5ccfc4 100644
--- a/askbot/utils/functions.py
+++ b/askbot/utils/functions.py
@@ -1,4 +1,7 @@
import re
+import datetime
+from django.utils.translation import ugettext as _
+from django.utils.translation import ungettext
def get_from_dict_or_object(source, key):
try:
@@ -43,3 +46,77 @@ def not_a_robot_request(request):
return True
return False
+
+def diff_date(date, limen=2, use_on_prefix = False):
+ now = datetime.datetime.now()#datetime(*time.localtime()[0:6])#???
+ diff = now - date
+ days = diff.days
+ hours = int(diff.seconds/3600)
+ minutes = int(diff.seconds/60)
+
+ if days > 2:
+ if date.year == now.year:
+ date_token = date.strftime("%b %d")
+ else:
+ date_token = date.strftime("%b %d '%y")
+ if use_on_prefix:
+ return _('on %(date)s') % { 'date': date_token }
+ else:
+ return date_token
+ elif days == 2:
+ return _('2 days ago')
+ elif days == 1:
+ return _('yesterday')
+ elif minutes >= 60:
+ return ungettext('%(hr)d hour ago','%(hr)d hours ago',hours) % {'hr':hours}
+ else:
+ return ungettext('%(min)d min ago','%(min)d mins ago',minutes) % {'min':minutes}
+
+#todo: this function may need to be removed to simplify the paginator functionality
+LEADING_PAGE_RANGE_DISPLAYED = TRAILING_PAGE_RANGE_DISPLAYED = 5
+LEADING_PAGE_RANGE = TRAILING_PAGE_RANGE = 4
+NUM_PAGES_OUTSIDE_RANGE = 1
+ADJACENT_PAGES = 2
+def setup_paginator(context):
+ """
+ custom paginator tag
+ Inspired from http://blog.localkinegrinds.com/2007/09/06/digg-style-pagination-in-django/
+ """
+ if (context["is_paginated"]):
+ " Initialize variables "
+ in_leading_range = in_trailing_range = False
+ pages_outside_leading_range = pages_outside_trailing_range = range(0)
+
+ if (context["pages"] <= LEADING_PAGE_RANGE_DISPLAYED):
+ in_leading_range = in_trailing_range = True
+ page_numbers = [n for n in range(1, context["pages"] + 1) if n > 0 and n <= context["pages"]]
+ elif (context["page"] <= LEADING_PAGE_RANGE):
+ in_leading_range = True
+ page_numbers = [n for n in range(1, LEADING_PAGE_RANGE_DISPLAYED + 1) if n > 0 and n <= context["pages"]]
+ pages_outside_leading_range = [n + context["pages"] for n in range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)]
+ elif (context["page"] > context["pages"] - TRAILING_PAGE_RANGE):
+ in_trailing_range = True
+ page_numbers = [n for n in range(context["pages"] - TRAILING_PAGE_RANGE_DISPLAYED + 1, context["pages"] + 1) if n > 0 and n <= context["pages"]]
+ pages_outside_trailing_range = [n + 1 for n in range(0, NUM_PAGES_OUTSIDE_RANGE)]
+ else:
+ page_numbers = [n for n in range(context["page"] - ADJACENT_PAGES, context["page"] + ADJACENT_PAGES + 1) if n > 0 and n <= context["pages"]]
+ pages_outside_leading_range = [n + context["pages"] for n in range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)]
+ pages_outside_trailing_range = [n + 1 for n in range(0, NUM_PAGES_OUTSIDE_RANGE)]
+
+ extend_url = context.get('extend_url', '')
+ return {
+ "base_url": context["base_url"],
+ "is_paginated": context["is_paginated"],
+ "previous": context["previous"],
+ "has_previous": context["has_previous"],
+ "next": context["next"],
+ "has_next": context["has_next"],
+ "page": context["page"],
+ "pages": context["pages"],
+ "page_numbers": page_numbers,
+ "in_leading_range" : in_leading_range,
+ "in_trailing_range" : in_trailing_range,
+ "pages_outside_leading_range": pages_outside_leading_range,
+ "pages_outside_trailing_range": pages_outside_trailing_range,
+ "extend_url" : extend_url
+ }
diff --git a/askbot/views/readers.py b/askbot/views/readers.py
index 6c4e6cce..e7f573ad 100644
--- a/askbot/views/readers.py
+++ b/askbot/views/readers.py
@@ -20,6 +20,7 @@ from django.utils.html import *
from django.utils import simplejson
from django.db.models import Q
from django.utils.translation import ugettext as _
+from django.utils import translation
from django.core.urlresolvers import reverse
from django.views.decorators.cache import cache_page
from django.core import exceptions as django_exceptions
@@ -40,6 +41,7 @@ from askbot.search.state_manager import SearchState
from askbot.templatetags import extra_tags
from askbot.templatetags import extra_filters
from askbot.conf import settings as askbot_settings
+from askbot.skins.loaders import ENV #jinja2 template loading enviroment
# used in index page
#todo: - take these out of const or settings
@@ -302,8 +304,16 @@ def questions(request):
tags_autocomplete = _get_tags_cache_json()
+ reset_method_count = 0
+ if search_state.query:
+ reset_method_count += 1
+ if search_state.tags:
+ reset_method_count += 1
+ if meta_data.get('author_name',None):
+ reset_method_count += 1
template_context = RequestContext(request, {
+ 'language_code': translation.get_language(),
'view_name': 'questions',
'active_tab': 'questions',
'questions' : questions,
@@ -325,13 +335,15 @@ def questions(request):
#todo: organize variables by type
if request.is_ajax():
+ #this branch should be dead now
+ raise NotImplementedError()
template = loader.get_template('questions_ajax.html')
question_snippet = template.render(template_context)
output = {'question_snippet': question_snippet}
#print simplejson.dumps(output)
return HttpResponse(simplejson.dumps(output), mimetype='application/json')
else:
- template = loader.get_template('questions.html')
+ template = ENV.get_template('questions.jinja.html')
return HttpResponse(template.render(template_context))
#after = datetime.datetime.now()
#print 'time to render %s' % (after - before)
diff --git a/coffin/.___init__.py b/coffin/.___init__.py
new file mode 100644
index 00000000..b44b5fae
--- /dev/null
+++ b/coffin/.___init__.py
Binary files differ
diff --git a/coffin/._common.py b/coffin/._common.py
new file mode 100644
index 00000000..c83780af
--- /dev/null
+++ b/coffin/._common.py
Binary files differ
diff --git a/coffin/._interop.py b/coffin/._interop.py
new file mode 100644
index 00000000..94121d1f
--- /dev/null
+++ b/coffin/._interop.py
Binary files differ
diff --git a/coffin/__init__.py b/coffin/__init__.py
new file mode 100644
index 00000000..df89bdd6
--- /dev/null
+++ b/coffin/__init__.py
@@ -0,0 +1,44 @@
+"""
+Coffin
+~~~~~~
+
+`Coffin <http://www.github.com/dcramer/coffin>` is a package that resolves the
+impedance mismatch between `Django <http://www.djangoproject.com/>` and `Jinja2
+<http://jinja.pocoo.org/2/>` through various adapters. The aim is to use Coffin
+as a drop-in replacement for Django's template system to whatever extent is
+reasonable.
+
+:copyright: 2008 by Christopher D. Leary
+:license: BSD, see LICENSE for more details.
+"""
+
+
+__all__ = ('__version__', '__build__', '__docformat__', 'get_revision')
+__version__ = (0, 3)
+__docformat__ = 'restructuredtext en'
+
+import os
+
+def _get_git_revision(path):
+ revision_file = os.path.join(path, 'refs', 'heads', 'master')
+ if not os.path.exists(revision_file):
+ return None
+ fh = open(revision_file, 'r')
+ try:
+ return fh.read()
+ finally:
+ fh.close()
+
+def get_revision():
+ """
+ :returns: Revision number of this branch/checkout, if available. None if
+ no revision number can be determined.
+ """
+ package_dir = os.path.dirname(__file__)
+ checkout_dir = os.path.normpath(os.path.join(package_dir, '..'))
+ path = os.path.join(checkout_dir, '.git')
+ if os.path.exists(path):
+ return _get_git_revision(path)
+ return None
+
+__build__ = get_revision()
diff --git a/coffin/common.py b/coffin/common.py
new file mode 100644
index 00000000..2e381ff3
--- /dev/null
+++ b/coffin/common.py
@@ -0,0 +1,148 @@
+import os
+import warnings
+
+from django import dispatch
+from jinja2 import Environment, loaders
+
+__all__ = ('env', 'need_env')
+
+env = None
+
+_JINJA_I18N_EXTENSION_NAME = 'jinja2.ext.i18n'
+
+# TODO: This should be documented (as even I'm not sure where it's use-case is)
+need_env = dispatch.Signal(providing_args=['arguments', 'loaders',
+ 'filters', 'extensions',
+ 'globals', 'tests'])
+
+class CoffinEnvironment(Environment):
+ def __init__(self, filters={}, globals={}, tests={}, loader=None, extensions=[], **kwargs):
+ if not loader:
+ loader = loaders.ChoiceLoader(self._get_loaders())
+ all_ext = self._get_all_extensions()
+
+ extensions.extend(all_ext['extensions'])
+ super(CoffinEnvironment, self).__init__(extensions=extensions, loader=loader, **kwargs)
+ self.filters.update(filters)
+ self.filters.update(all_ext['filters'])
+ self.globals.update(globals)
+ self.globals.update(all_ext['globals'])
+ self.tests.update(tests)
+ self.tests.update(all_ext['tests'])
+
+ from coffin.template import Template as CoffinTemplate
+ self.template_class = CoffinTemplate
+
+ def _get_loaders(self):
+ """Tries to translate each template loader given in the Django settings
+ (:mod:`django.settings`) to a similarly-behaving Jinja loader.
+ Warns if a similar loader cannot be found.
+ Allows for Jinja2 loader instances to be placed in the template loader
+ settings.
+ """
+ loaders = []
+
+ from coffin.template.loaders import jinja_loader_from_django_loader
+
+ from django.conf import settings
+ for loader in settings.TEMPLATE_LOADERS:
+ if isinstance(loader, basestring):
+ loader_obj = jinja_loader_from_django_loader(loader)
+ if loader_obj:
+ loaders.append(loader_obj)
+ else:
+ warnings.warn('Cannot translate loader: %s' % loader)
+ else: # It's assumed to be a Jinja2 loader instance.
+ loaders.append(loader)
+ return loaders
+
+
+ def _get_templatelibs(self):
+ """Return an iterable of template ``Library`` instances.
+
+ Since we cannot support the {% load %} tag in Jinja, we have to
+ register all libraries globally.
+ """
+ from django.conf import settings
+ from django.template import get_library, InvalidTemplateLibrary
+
+ libs = []
+ for a in settings.INSTALLED_APPS:
+ try:
+ path = __import__(a + '.templatetags', {}, {}, ['__file__']).__file__
+ path = os.path.dirname(path) # we now have the templatetags/ directory
+ except ImportError:
+ pass
+ else:
+ for f in os.listdir(path):
+ if f == '__init__.py':
+ continue
+ if f.endswith('.py'):
+ try:
+ # TODO: will need updating when #6587 lands
+ libs.append(get_library(
+ "django.templatetags.%s" % os.path.splitext(f)[0]))
+ except InvalidTemplateLibrary:
+ pass
+ return libs
+
+ def _get_all_extensions(self):
+ from django.conf import settings
+ from coffin.template import builtins
+ from django.core.urlresolvers import get_callable
+
+ extensions, filters, globals, tests = [], {}, {}, {}
+
+ # start with our builtins
+ for lib in builtins:
+ extensions.extend(getattr(lib, 'jinja2_extensions', []))
+ filters.update(getattr(lib, 'jinja2_filters', {}))
+ globals.update(getattr(lib, 'jinja2_globals', {}))
+ tests.update(getattr(lib, 'jinja2_tests', {}))
+
+ if settings.USE_I18N:
+ extensions.append(_JINJA_I18N_EXTENSION_NAME)
+
+ # add the globally defined extension list
+ extensions.extend(list(getattr(settings, 'JINJA2_EXTENSIONS', [])))
+
+ def from_setting(setting):
+ retval = {}
+ setting = getattr(settings, setting, {})
+ if isinstance(setting, dict):
+ for key, value in setting.iteritems():
+ retval[user] = callable(value) and value or get_callable(value)
+ else:
+ for value in setting:
+ value = callable(value) and value or get_callable(value)
+ retval[value.__name__] = value
+ return retval
+
+ filters.update(from_setting('JINJA2_FILTERS'))
+ globals.update(from_setting('JINJA2_GLOBALS'))
+ tests.update(from_setting('JINJA2_TESTS'))
+
+ # add extensions defined in application's templatetag libraries
+ for lib in self._get_templatelibs():
+ extensions.extend(getattr(lib, 'jinja2_extensions', []))
+ filters.update(getattr(lib, 'jinja2_filters', {}))
+ globals.update(getattr(lib, 'jinja2_globals', {}))
+ tests.update(getattr(lib, 'jinja2_tests', {}))
+
+ return dict(
+ extensions=extensions,
+ filters=filters,
+ globals=globals,
+ tests=tests,
+ )
+
+def get_env():
+ """
+ :return: A Jinja2 environment singleton.
+ """
+ # need_env.send(sender=Environment, arguments=arguments,
+ # loaders=loaders_, extensions=extensions,
+ # filters=filters, tests=tests, globals=globals)
+ return CoffinEnvironment(autoescape=True)
+
+env = get_env()
diff --git a/coffin/conf/.___init__.py b/coffin/conf/.___init__.py
new file mode 100644
index 00000000..c6b47018
--- /dev/null
+++ b/coffin/conf/.___init__.py
Binary files differ
diff --git a/coffin/conf/__init__.py b/coffin/conf/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/conf/__init__.py
diff --git a/coffin/conf/urls/__init__.py b/coffin/conf/urls/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/conf/urls/__init__.py
diff --git a/coffin/conf/urls/defaults.py b/coffin/conf/urls/defaults.py
new file mode 100644
index 00000000..3049e2b9
--- /dev/null
+++ b/coffin/conf/urls/defaults.py
@@ -0,0 +1,4 @@
+from django.conf.urls.defaults import *
+
+handler404 = 'coffin.views.defaults.page_not_found'
+handler500 = 'coffin.views.defaults.server_error' \ No newline at end of file
diff --git a/coffin/contrib/__init__.py b/coffin/contrib/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/contrib/__init__.py
diff --git a/coffin/contrib/markup/__init__.py b/coffin/contrib/markup/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/contrib/markup/__init__.py
diff --git a/coffin/contrib/markup/models.py b/coffin/contrib/markup/models.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/contrib/markup/models.py
diff --git a/coffin/contrib/markup/templatetags/__init__.py b/coffin/contrib/markup/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/contrib/markup/templatetags/__init__.py
diff --git a/coffin/contrib/markup/templatetags/markup.py b/coffin/contrib/markup/templatetags/markup.py
new file mode 100644
index 00000000..0d6b92f9
--- /dev/null
+++ b/coffin/contrib/markup/templatetags/markup.py
@@ -0,0 +1,15 @@
+"""Makes the template filters from the ``django.contrib.markup`` app
+available to both the Jinja2 and Django engines.
+
+In other words, adding ``coffin.contrib.markup`` to your INSTALLED_APPS
+setting will enable the markup filters not only through Coffin, but
+also through the default Django template system.
+"""
+
+from coffin.template import Library as CoffinLibrary
+from django.contrib.markup.templatetags.markup import register
+
+
+# Convert Django's Library into a Coffin Library object, which will
+# make sure the filters are correctly ported to Jinja2.
+register = CoffinLibrary.from_django(register) \ No newline at end of file
diff --git a/coffin/contrib/syndication/__init__.py b/coffin/contrib/syndication/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/contrib/syndication/__init__.py
diff --git a/coffin/contrib/syndication/feeds.py b/coffin/contrib/syndication/feeds.py
new file mode 100644
index 00000000..f8f0701f
--- /dev/null
+++ b/coffin/contrib/syndication/feeds.py
@@ -0,0 +1,36 @@
+from django.contrib.syndication.feeds import * # merge modules
+
+import sys
+from django.contrib.syndication.feeds import Feed as DjangoFeed
+from coffin.template import loader as coffin_loader
+
+
+class Feed(DjangoFeed):
+ """A ``Feed`` implementation that renders it's title and
+ description templates using Jinja2.
+
+ Unfortunately, Django's base ``Feed`` class is not very extensible
+ in this respect at all. For a real solution, we'd have to essentially
+ have to duplicate the whole class. So for now, we use this terrible
+ non-thread safe hack.
+
+ Another, somewhat crazy option would be:
+ * Render the templates ourselves through Jinja2 (possible
+ introduce new attributes to avoid having to rewrite the
+ existing ones).
+ * Make the rendered result available to Django/the superclass by
+ using a custom template loader using a prefix, say
+ "feed:<myproject.app.views.MyFeed>". The loader would simply
+ return the Jinja-rendered template (escaped), the Django template
+ mechanism would find no nodes and just pass the output through.
+ Possible even worse than this though.
+ """
+
+ def get_feed(self, *args, **kwargs):
+ parent_module = sys.modules[DjangoFeed.__module__]
+ old_loader = parent_module.loader
+ parent_module.loader = coffin_loader
+ try:
+ return super(Feed, self).get_feed(*args, **kwargs)
+ finally:
+ parent_module.loader = old_loader \ No newline at end of file
diff --git a/coffin/interop.py b/coffin/interop.py
new file mode 100644
index 00000000..54120004
--- /dev/null
+++ b/coffin/interop.py
@@ -0,0 +1,120 @@
+"""Compatibility functions between Jinja2 and Django.
+
+General notes:
+
+ - The Django ``stringfilter`` decorator is supported, but should not be
+ used when writing filters specifically for Jinja: It will lose the
+ attributes attached to the filter function by Jinja's
+ ``environmentfilter`` and ``contextfilter`` decorators, when used
+ in the wrong order.
+
+ Maybe coffin should provide a custom version of stringfilter.
+
+ - While transparently converting filters between Django and Jinja works
+ for the most part, there is an issue with Django's
+ ``mark_for_escaping``, as Jinja does not support a similar mechanism.
+ Instead, for Jinja, we escape such strings immediately (whereas Django
+ defers it to the template engine).
+"""
+
+import inspect
+from django.utils.safestring import SafeUnicode, SafeData, EscapeData
+from jinja2 import Markup, environmentfilter
+
+
+__all__ = (
+ 'DJANGO', 'JINJA2',
+ 'django_filter_to_jinja2',
+ 'jinja2_filter_to_django',
+ 'guess_filter_type',)
+
+
+DJANGO = 'django'
+JINJA2 = 'jinja2'
+
+
+def django_filter_to_jinja2(filter_func):
+ """
+ Note: Due to the way this function is used by
+ ``coffin.template.Library``, it needs to be able to handle native
+ Jinja2 filters and pass them through unmodified. This necessity
+ stems from the fact that it is not always possible to determine
+ the type of a filter.
+
+ TODO: Django's "func.is_safe" is not yet handled
+ """
+ def _convert(v):
+ if isinstance(v, SafeData):
+ return Markup(v)
+ if isinstance(v, EscapeData):
+ return Markup.escape(v) # not 100% equivalent, see mod docs
+ return v
+ def conversion_wrapper(*args, **kwargs):
+ result = filter_func(*args, **kwargs)
+ return _convert(result)
+ # Jinja2 supports a similar machanism to Django's
+ # ``needs_autoescape`` filters: environment filters. We can
+ # thus support Django filters that use it in Jinja2 with just
+ # a little bit of argument rewriting.
+ if hasattr(filter_func, 'needs_autoescape'):
+ @environmentfilter
+ def autoescape_wrapper(environment, *args, **kwargs):
+ kwargs['autoescape'] = environment.autoescape
+ return conversion_wrapper(*args, **kwargs)
+ return autoescape_wrapper
+ else:
+ return conversion_wrapper
+
+
+def jinja2_filter_to_django(filter_func):
+ """
+ Note: Due to the way this function is used by
+ ``coffin.template.Library``, it needs to be able to handle native
+ Django filters and pass them through unmodified. This necessity
+ stems from the fact that it is not always possible to determine
+ the type of a filter.
+ """
+ if guess_filter_type(filter_func)[0] == DJANGO:
+ return filter_func
+ def _convert(v):
+ # TODO: for now, this is not even necessary: Markup strings have
+ # a custom replace() method that is immume to Django's escape()
+ # attempts.
+ #if isinstance(v, Markup):
+ # return SafeUnicode(v) # jinja is always unicode
+ # ... Jinja does not have a EscapeData equivalent
+ return v
+ def wrapped(value, *args, **kwargs):
+ result = filter_func(value, *args, **kwargs)
+ return _convert(result)
+ return wrapped
+
+
+def guess_filter_type(filter_func):
+ """Returns a 2-tuple of (type, can_be_ported).
+
+ ``type`` is one of DJANGO, JINJA2, or ``False`` if the type can
+ not be determined.
+
+ ``can_be_ported`` is ``True`` if we believe the filter could be
+ ported to the other engine, respectively, or ``False`` if we know
+ it can't.
+
+ TODO: May not yet use all possible clues, e.g. decorators like
+ ``stringfilter``.
+ TOOD: Needs tests.
+ """
+ if hasattr(filter_func, 'contextfilter') or \
+ hasattr(filter_func, 'environmentfilter'):
+ return JINJA2, False
+
+ args = inspect.getargspec(filter_func)
+ if len(args[0]) - (len(args[3]) if args[3] else 0) > 2:
+ return JINJA2, False
+
+ if hasattr(filter_func, 'needs_autoescape'):
+ return DJANGO, True
+
+ # Looks like your run of the mill Python function, which are
+ # easily convertible in either direction.
+ return False, True \ No newline at end of file
diff --git a/coffin/shortcuts/.___init__.py b/coffin/shortcuts/.___init__.py
new file mode 100644
index 00000000..c21832ed
--- /dev/null
+++ b/coffin/shortcuts/.___init__.py
Binary files differ
diff --git a/coffin/shortcuts/__init__.py b/coffin/shortcuts/__init__.py
new file mode 100644
index 00000000..c882dfcd
--- /dev/null
+++ b/coffin/shortcuts/__init__.py
@@ -0,0 +1,25 @@
+from django.http import HttpResponse
+
+# Merge with original namespace so user
+# doesn't have to import twice.
+from django.shortcuts import *
+
+
+__all__ = ('render_to_string', 'render_to_response',)
+
+
+# Is within ``template.loader`` as per Django specification -
+# but I think it fits very well here.
+from coffin.template.loader import render_to_string
+
+
+def render_to_response(template_name, dictionary=None, context_instance=None,
+ mimetype=None):
+ """
+ :param template_name: Filename of the template to get or a sequence of
+ filenames to try, in order.
+ :param dictionary: Rendering context for the template.
+ :returns: A response object with the evaluated template as a payload.
+ """
+ rendered = render_to_string(template_name, dictionary, context_instance)
+ return HttpResponse(rendered, mimetype=mimetype)
diff --git a/coffin/template/.___init__.py b/coffin/template/.___init__.py
new file mode 100644
index 00000000..87947898
--- /dev/null
+++ b/coffin/template/.___init__.py
Binary files differ
diff --git a/coffin/template/._defaultfilters.py b/coffin/template/._defaultfilters.py
new file mode 100644
index 00000000..8fd7d746
--- /dev/null
+++ b/coffin/template/._defaultfilters.py
Binary files differ
diff --git a/coffin/template/._defaulttags.py b/coffin/template/._defaulttags.py
new file mode 100644
index 00000000..db9dcec7
--- /dev/null
+++ b/coffin/template/._defaulttags.py
Binary files differ
diff --git a/coffin/template/._library.py b/coffin/template/._library.py
new file mode 100644
index 00000000..18fe8260
--- /dev/null
+++ b/coffin/template/._library.py
Binary files differ
diff --git a/coffin/template/._loader.py b/coffin/template/._loader.py
new file mode 100644
index 00000000..3b699201
--- /dev/null
+++ b/coffin/template/._loader.py
Binary files differ
diff --git a/coffin/template/._loaders.py b/coffin/template/._loaders.py
new file mode 100644
index 00000000..e17a06d5
--- /dev/null
+++ b/coffin/template/._loaders.py
Binary files differ
diff --git a/coffin/template/__init__.py b/coffin/template/__init__.py
new file mode 100644
index 00000000..b487c8f5
--- /dev/null
+++ b/coffin/template/__init__.py
@@ -0,0 +1,93 @@
+from django.template import (
+ Context as DjangoContext,
+ add_to_builtins as django_add_to_builtins,
+ get_library)
+from jinja2 import Template as _Jinja2Template
+
+# Merge with ``django.template``.
+from django.template import __all__
+from django.template import *
+
+# Override default library class with ours
+from library import *
+
+
+class Template(_Jinja2Template):
+ """Fixes the incompabilites between Jinja2's template class and
+ Django's.
+
+ The end result should be a class that renders Jinja2 templates but
+ is compatible with the interface specfied by Django.
+
+ This includes flattening a ``Context`` instance passed to render
+ and making sure that this class will automatically use the global
+ coffin environment.
+ """
+
+ def __new__(cls, template_string, origin=None, name=None):
+ # We accept the "origin" and "name" arguments, but discard them
+ # right away - Jinja's Template class (apparently) stores no
+ # equivalent information.
+ from coffin.common import env
+
+ return env.from_string(template_string, template_class=cls)
+
+ def __iter__(self):
+ # TODO: Django allows iterating over the templates nodes. Should
+ # be parse ourself and iterate over the AST?
+ raise NotImplementedError()
+
+ def render(self, context=None):
+ """Differs from Django's own render() slightly in that makes the
+ ``context`` parameter optional. We try to strike a middle ground
+ here between implementing Django's interface while still supporting
+ Jinja's own call syntax as well.
+ """
+ if context is None:
+ context = {}
+ else:
+ context = dict_from_django_context(context)
+ assert isinstance(context, dict) # Required for **-operator.
+ return super(Template, self).render(**context)
+
+
+def dict_from_django_context(context):
+ """Flattens a Django :class:`django.template.context.Context` object.
+ """
+ if not isinstance(context, DjangoContext):
+ return context
+ else:
+ dict_ = {}
+ # Newest dicts are up front, so update from oldest to newest.
+ for subcontext in reversed(list(context)):
+ dict_.update(dict_from_django_context(subcontext))
+ return dict_
+
+
+# libraries to load by default for a new environment
+builtins = []
+
+
+def add_to_builtins(module_name):
+ """Add the given module to both Coffin's list of default template
+ libraries as well as Django's. This makes sense, since Coffin
+ libs are compatible with Django libraries.
+
+ You can still use Django's own ``add_to_builtins`` to register
+ directly with Django and bypass Coffin.
+
+ TODO: Allow passing path to (or reference of) extensions and
+ filters directly. This would make it easier to use this function
+ with 3rd party Jinja extensions that do not know about Coffin and
+ thus will not provide a Library object.
+
+ XXX/TODO: Why do we need our own custom list of builtins? Our
+ Library object is compatible, remember!? We can just add them
+ directly to Django's own list of builtins.
+ """
+ builtins.append(get_library(module_name))
+ django_add_to_builtins(module_name)
+
+
+add_to_builtins('coffin.template.defaulttags')
+add_to_builtins('coffin.template.defaultfilters') \ No newline at end of file
diff --git a/coffin/template/defaultfilters.py b/coffin/template/defaultfilters.py
new file mode 100644
index 00000000..c566a7d2
--- /dev/null
+++ b/coffin/template/defaultfilters.py
@@ -0,0 +1,99 @@
+"""Jinja2-ports of many of Django's default filters.
+
+TODO: Most of the filters in here need to be updated for autoescaping.
+"""
+
+from coffin.template import Library
+from jinja2.runtime import Undefined
+# from jinja2 import Markup
+
+register = Library()
+
+@register.filter(jinja2_only=True)
+def url(view_name, *args, **kwargs):
+ from coffin.template.defaulttags import url
+ return url._reverse(view_name, args, kwargs)
+
+@register.filter(jinja2_only=True)
+def timesince(value, arg=None):
+ if value is None or isinstance(value, Undefined):
+ return u''
+ from django.utils.timesince import timesince
+ if arg:
+ return timesince(value, arg)
+ return timesince(value)
+
+@register.filter(jinja2_only=True)
+def timeuntil(value, arg=None):
+ if value is None or isinstance(value, Undefined):
+ return u''
+ from django.utils.timesince import timeuntil
+ return timeuntil(date, arg)
+
+@register.filter(jinja2_only=True)
+def date(value, arg=None):
+ if value is None or isinstance(value, Undefined):
+ return u''
+ from django.conf import settings
+ from django.utils.dateformat import format
+ if arg is None:
+ arg = settings.DATE_FORMAT
+ return format(value, arg)
+
+@register.filter(jinja2_only=True)
+def time(value, arg=None):
+ if value is None or isinstance(value, Undefined):
+ return u''
+ from django.conf import settings
+ from django.utils.dateformat import time_format
+ if arg is None:
+ arg = settings.TIME_FORMAT
+ return time_format(value, arg)
+
+@register.filter(jinja2_only=True)
+def truncatewords(value, length):
+ # Jinja2 has it's own ``truncate`` filter that supports word
+ # boundaries and more stuff, but cannot deal with HTML.
+ from django.utils.text import truncate_words
+ return truncate_words(value, int(length))
+
+@register.filter(jinja2_only=True)
+def truncatewords_html(value, length):
+ from django.utils.text import truncate_html_words
+ return truncate_html_words(value, int(length))
+
+@register.filter(jinja2_only=True)
+def pluralize(value, s1='s', s2=None):
+ """Like Django's pluralize-filter, but instead of using an optional
+ comma to separate singular and plural suffixes, it uses two distinct
+ parameters.
+
+ It also is less forgiving if applied to values that do not allow
+ making a decision between singular and plural.
+ """
+ if s2 is not None:
+ singular_suffix, plural_suffix = s1, s2
+ else:
+ plural_suffix = s1
+ singular_suffix = ''
+
+ try:
+ if int(value) != 1:
+ return plural_suffix
+ except TypeError: # not a string or a number; maybe it's a list?
+ if len(value) != 1:
+ return plural_suffix
+ return singular_suffix
+
+@register.filter(jinja2_only=True)
+def floatformat(value, arg=-1):
+ """Builds on top of Django's own version, but adds strict error
+ checking, staying with the philosophy.
+ """
+ from django.template.defaultfilters import floatformat
+ from coffin.interop import django_filter_to_jinja2
+ arg = int(arg) # raise exception
+ result = django_filter_to_jinja2(floatformat)(value, arg)
+ if result == '': # django couldn't handle the value
+ raise ValueError(value)
+ return result \ No newline at end of file
diff --git a/coffin/template/defaulttags.py b/coffin/template/defaulttags.py
new file mode 100644
index 00000000..b9994257
--- /dev/null
+++ b/coffin/template/defaulttags.py
@@ -0,0 +1,364 @@
+from jinja2 import nodes
+from jinja2.ext import Extension
+from jinja2.exceptions import TemplateSyntaxError
+from django.conf import settings
+from coffin.template import Library
+
+
+class LoadExtension(Extension):
+ """The load-tag is a no-op in Coffin. Instead, all template libraries
+ are always loaded.
+
+ Note: Supporting a functioning load-tag in Jinja is tough, though
+ theoretically possible. The trouble is activating new extensions while
+ parsing is ongoing. The ``Parser.extensions`` dict of the current
+ parser instance needs to be modified, but apparently the only way to
+ get access would be by hacking the stack.
+ """
+
+ tags = set(['load'])
+
+ def parse(self, parser):
+ while not parser.stream.current.type == 'block_end':
+ parser.stream.next()
+ return []
+
+
+"""class AutoescapeExtension(Extension):
+ ""#"
+ Template to output works in three phases in Jinja2: parsing,
+ generation (compilation, AST-traversal), and rendering (execution).
+
+ Unfortunatly, the environment ``autoescape`` option comes into effect
+ during traversal, the part where we happen to have basically no control
+ over as an extension. It determines whether output is wrapped in
+ ``escape()`` calls.
+
+ Solutions that could possibly work:
+
+ * This extension could preprocess it's childnodes and wrap
+ everything output related inside the appropriate
+ ``Markup()`` or escape() call.
+
+ * We could use the ``preprocess`` hook to insert the
+ appropriate ``|safe`` and ``|escape`` filters on a
+ string-basis. This is very unlikely to work well.
+
+ There's also the issue of inheritance and just generally the nesting
+ of autoescape-tags to consider.
+
+ Other things of note:
+
+ * We can access ``parser.environment``, but that would only
+ affect the **parsing** of our child nodes.
+
+ * In the commented-out code below we are trying to affect the
+ autoescape setting during rendering. As noted, this could be
+ necessary for rare border cases where custom extension use
+ the autoescape attribute.
+
+ Both the above things would break Environment thread-safety though!
+
+ Overall, it's not looking to good for this extension.
+ ""#"
+
+ tags = ['autoescape']
+
+ def parse(self, parser):
+ lineno = parser.stream.next().lineno
+
+ old_autoescape = parser.environment.autoescape
+ parser.environment.autoescape = True
+ try:
+ body = parser.parse_statements(
+ ['name:endautoescape'], drop_needle=True)
+ finally:
+ parser.environment.autoescape = old_autoescape
+
+ # Not sure yet if the code below is necessary - it changes
+ # environment.autoescape during template rendering. If for example
+ # a CallBlock function accesses ``environment.autoescape``, it
+ # presumably is.
+ # This also should use try-finally though, which Jinja's API
+ # doesn't support either. We could fake that as well by using
+ # InternalNames that output the necessary indentation and keywords,
+ # but at this point it starts to get really messy.
+ #
+ # TODO: Actually, there's ``nodes.EnvironmentAttribute``.
+ #ae_setting = object.__new__(nodes.InternalName)
+ #nodes.Node.__init__(ae_setting, 'environment.autoescape', lineno=lineno)
+ #temp = parser.free_identifier()
+ #body.insert(0, nodes.Assign(temp, ae_setting))
+ #body.insert(1, nodes.Assign(ae_setting, nodes.Const(True)))
+ #body.insert(len(body), nodes.Assign(ae_setting, temp))
+ return body
+"""
+
+
+class URLExtension(Extension):
+ """Returns an absolute URL matching given view with its parameters.
+
+ This is a way to define links that aren't tied to a particular URL
+ configuration::
+
+ {% url path.to.some_view arg1,arg2,name1=value1 %}
+
+ Known differences to Django's url-Tag:
+
+ - In Django, the view name may contain any non-space character.
+ Since Jinja's lexer does not identify whitespace to us, only
+ characters that make up valid identifers, plus dots and hyphens
+ are allowed. Note that identifers in Jinja 2 may not contain
+ non-ascii characters.
+
+ As an alternative, you may specifify the view as a string,
+ which bypasses all these restrictions. It further allows you
+ to apply filters:
+
+ {% url "меткаda.some-view"|afilter %}
+ """
+
+ tags = set(['url'])
+
+ def parse(self, parser):
+ stream = parser.stream
+
+ tag = stream.next()
+
+ # get view name
+ if stream.current.test('string'):
+ viewname = parser.parse_primary()
+ else:
+ # parse valid tokens and manually build a string from them
+ bits = []
+ name_allowed = True
+ while True:
+ if stream.current.test_any('dot', 'sub'):
+ bits.append(stream.next())
+ name_allowed = True
+ elif stream.current.test('name') and name_allowed:
+ bits.append(stream.next())
+ name_allowed = False
+ else:
+ break
+ viewname = nodes.Const("".join([b.value for b in bits]))
+ if not bits:
+ raise TemplateSyntaxError("'%s' requires path to view" %
+ tag.value, tag.lineno)
+
+ # get arguments
+ args = []
+ kwargs = []
+ while not stream.current.test_any('block_end', 'name:as'):
+ if args or kwargs:
+ stream.expect('comma')
+ if stream.current.test('name') and stream.look().test('assign'):
+ key = nodes.Const(stream.next().value)
+ stream.skip()
+ value = parser.parse_expression()
+ kwargs.append(nodes.Pair(key, value, lineno=key.lineno))
+ else:
+ args.append(parser.parse_expression())
+
+ make_call_node = lambda *kw: \
+ self.call_method('_reverse',
+ args=[viewname, nodes.List(args), nodes.Dict(kwargs)],
+ kwargs=kw)
+
+ # if an as-clause is specified, write the result to context...
+ if stream.next_if('name:as'):
+ var = nodes.Name(stream.expect('name').value, 'store')
+ call_node = make_call_node(nodes.Keyword('fail', nodes.Const(False)))
+ return nodes.Assign(var, call_node)
+ # ...otherwise print it out.
+ else:
+ return nodes.Output([make_call_node()]).set_lineno(tag.lineno)
+
+ @classmethod
+ def _reverse(self, viewname, args, kwargs, fail=True):
+ from django.core.urlresolvers import reverse, NoReverseMatch
+
+ # Try to look up the URL twice: once given the view name,
+ # and again relative to what we guess is the "main" app.
+ url = ''
+ try:
+ url = reverse(viewname, args=args, kwargs=kwargs)
+ except NoReverseMatch:
+ projectname = settings.SETTINGS_MODULE.split('.')[0]
+ try:
+ url = reverse(projectname + '.' + viewname,
+ args=args, kwargs=kwargs)
+ except NoReverseMatch:
+ if fail:
+ raise
+ else:
+ return ''
+
+ return url
+
+
+class WithExtension(Extension):
+ """Adds a value to the context (inside this block) for caching and
+ easy access, just like the Django-version does.
+
+ For example::
+
+ {% with person.some_sql_method as total %}
+ {{ total }} object{{ total|pluralize }}
+ {% endwith %}
+
+ TODO: The new Scope node introduced in Jinja2 6334c1eade73 (the 2.2
+ dev version) would help here, but we don't want to rely on that yet.
+ See also:
+ http://dev.pocoo.org/projects/jinja/browser/tests/test_ext.py
+ http://dev.pocoo.org/projects/jinja/ticket/331
+ http://dev.pocoo.org/projects/jinja/ticket/329
+ """
+
+ tags = set(['with'])
+
+ def parse(self, parser):
+ lineno = parser.stream.next().lineno
+
+ value = parser.parse_expression()
+ parser.stream.expect('name:as')
+ name = parser.stream.expect('name')
+
+ body = parser.parse_statements(['name:endwith'], drop_needle=True)
+ return nodes.CallBlock(
+ self.call_method('_render_block', args=[value]),
+ [nodes.Name(name.value, 'store')], [], body).\
+ set_lineno(lineno)
+
+ def _render_block(self, value, caller=None):
+ return caller(value)
+
+
+class CacheExtension(Extension):
+ """Exactly like Django's own tag, but supports full Jinja2
+ expressiveness for all arguments.
+
+ {% cache gettimeout()*2 "foo"+options.cachename %}
+ ...
+ {% endcache %}
+
+ This actually means that there is a considerable incompatibility
+ to Django: In Django, the second argument is simply a name, but
+ interpreted as a literal string. This tag, with Jinja2 stronger
+ emphasis on consistent syntax, requires you to actually specify the
+ quotes around the name to make it a string. Otherwise, allowing
+ Jinja2 expressions would be very hard to impossible (one could use
+ a lookahead to see if the name is followed by an operator, and
+ evaluate it as an expression if so, or read it as a string if not.
+ TODO: This may not be the right choice. Supporting expressions
+ here is probably not very important, so compatibility should maybe
+ prevail. Unfortunately, it is actually pretty hard to be compatibly
+ in all cases, simply because Django's per-character parser will
+ just eat everything until the next whitespace and consider it part
+ of the fragment name, while we have to work token-based: ``x*2``
+ would actually be considered ``"x*2"`` in Django, while Jinja2
+ would give us three tokens: ``x``, ``*``, ``2``.
+
+ General Syntax:
+
+ {% cache [expire_time] [fragment_name] [var1] [var2] .. %}
+ .. some expensive processing ..
+ {% endcache %}
+
+ Available by default (does not need to be loaded).
+
+ Partly based on the ``FragmentCacheExtension`` from the Jinja2 docs.
+
+ TODO: Should there be scoping issues with the internal dummy macro
+ limited access to certain outer variables in some cases, there is a
+ different way to write this. Generated code would look like this:
+
+ internal_name = environment.extensions['..']._get_cache_value():
+ if internal_name is not None:
+ yield internal_name
+ else:
+ internal_name = "" # or maybe use [] and append() for performance
+ internalname += "..."
+ internalname += "..."
+ internalname += "..."
+ environment.extensions['..']._set_cache_value(internalname):
+ yield internalname
+
+ In other words, instead of using a CallBlock which uses a local
+ function and calls into python, we have to separate calls into
+ python, but put the if-else logic itself into the compiled template.
+ """
+
+ tags = set(['cache'])
+
+ def parse(self, parser):
+ lineno = parser.stream.next().lineno
+
+ expire_time = parser.parse_expression()
+ fragment_name = parser.parse_expression()
+ vary_on = []
+ while not parser.stream.current.test('block_end'):
+ vary_on.append(parser.parse_expression())
+
+ body = parser.parse_statements(['name:endcache'], drop_needle=True)
+
+ return nodes.CallBlock(
+ self.call_method('_cache_support',
+ [expire_time, fragment_name,
+ nodes.List(vary_on), nodes.Const(lineno)]),
+ [], [], body).set_lineno(lineno)
+
+ def _cache_support(self, expire_time, fragm_name, vary_on, lineno, caller):
+ from django.core.cache import cache # delay depending in settings
+ from django.utils.http import urlquote
+
+ try:
+ expire_time = int(expire_time)
+ except (ValueError, TypeError):
+ raise TemplateSyntaxError('"%s" tag got a non-integer '
+ 'timeout value: %r' % (list(self.tags)[0], expire_time), lineno)
+
+ cache_key = u':'.join([fragm_name] + [urlquote(v) for v in vary_on])
+ value = cache.get(cache_key)
+ if value is None:
+ value = caller()
+ cache.set(cache_key, value, expire_time)
+ return value
+
+
+class SpacelessExtension(Extension):
+ """Removes whitespace between HTML tags, including tab and
+ newline characters.
+
+ Works exactly like Django's own tag.
+ """
+
+ tags = ['spaceless']
+
+ def parse(self, parser):
+ lineno = parser.stream.next().lineno
+ body = parser.parse_statements(['name:endspaceless'], drop_needle=True)
+ return nodes.CallBlock(
+ self.call_method('_strip_spaces', [], [], None, None),
+ [], [], body
+ ).set_lineno(lineno)
+
+ def _strip_spaces(self, caller=None):
+ from django.utils.html import strip_spaces_between_tags
+ return strip_spaces_between_tags(caller().strip())
+
+
+# nicer import names
+load = LoadExtension
+url = URLExtension
+with_ = WithExtension
+cache = CacheExtension
+spaceless = SpacelessExtension
+
+
+register = Library()
+register.tag(load)
+register.tag(url)
+register.tag(with_)
+register.tag(cache)
+register.tag(spaceless) \ No newline at end of file
diff --git a/coffin/template/library.py b/coffin/template/library.py
new file mode 100644
index 00000000..8e80edc5
--- /dev/null
+++ b/coffin/template/library.py
@@ -0,0 +1,215 @@
+from django.template import Library as DjangoLibrary, InvalidTemplateLibrary
+from jinja2.ext import Extension as Jinja2Extension
+from coffin.interop import (
+ DJANGO, JINJA2,
+ guess_filter_type, jinja2_filter_to_django, django_filter_to_jinja2)
+
+
+__all__ = ['Library']
+
+
+class Library(DjangoLibrary):
+ """Version of the Django ``Library`` class that can handle both
+ Django template engine tags and filters, as well as Jinja2
+ extensions and filters.
+
+ Tries to present a common registration interface to the extension
+ author, but provides both template engines with only those
+ components they can support.
+
+ Since custom Django tags and Jinja2 extensions are two completely
+ different beasts, they are handled completely separately. You can
+ register custom Django tags as usual, for example:
+
+ register.tag('current_time', do_current_time)
+
+ Or register a Jinja2 extension like this:
+
+ register.tag(CurrentTimeNode)
+
+ Filters, on the other hand, work similarily in both engines, and
+ for the most one can't tell whether a filter function was written
+ for Django or Jinja2. A compatibility layer is used to make to
+ make the filters you register usuable with both engines:
+
+ register.filter('cut', cut)
+
+ However, some of the more powerful filters just won't work in
+ Django, for example if more than one argument is required, or if
+ context- or environmentfilters are used. If ``cut`` in the above
+ example where such an extended filter, it would only be registered
+ with Jinja.
+
+ See also the module documentation for ``coffin.interop`` for
+ information on some of the limitations of this conversion.
+
+ TODO: Jinja versions of the ``simple_tag`` and ``inclusion_tag``
+ helpers would be nice, though since custom tags are not needed as
+ often in Jinja, this is not urgent.
+ """
+
+ def __init__(self):
+ super(Library, self).__init__()
+ self.jinja2_filters = {}
+ self.jinja2_extensions = []
+ self.jinja2_globals = {}
+ self.jinja2_tests = {}
+
+ @classmethod
+ def from_django(cls, django_library):
+ """Create a Coffin library object from a Django library.
+
+ Specifically, this ensures that filters already registered
+ with the Django library are also made available to Jinja,
+ where applicable.
+ """
+ from copy import copy
+ result = cls()
+ result.filters = copy(django_library.filters)
+ result.tags = copy(django_library.tags)
+ for name, func in result.filters.iteritems():
+ result._register_filter(name, func, jinja2_only=True)
+ return result
+
+ def test(self, name=None, func=None):
+ def inner(f):
+ name = getattr(f, "_decorated_function", f).__name__
+ self.jinja2_tests[name] = f
+ return f
+ if name == None and func == None:
+ # @register.test()
+ return inner
+ elif func == None:
+ if (callable(name)):
+ # register.test()
+ return inner(name)
+ else:
+ # @register.test('somename') or @register.test(name='somename')
+ def dec(func):
+ return self.test(name, func)
+ return dec
+ elif name != None and func != None:
+ # register.filter('somename', somefunc)
+ self.jinja2_tests[name] = func
+ return func
+ else:
+ raise InvalidTemplateLibrary("Unsupported arguments to "
+ "Library.test: (%r, %r)", (name, func))
+
+ def object(self, name=None, func=None):
+ def inner(f):
+ name = getattr(f, "_decorated_function", f).__name__
+ self.jinja2_globals[name] = f
+ return f
+ if name == None and func == None:
+ # @register.object()
+ return inner
+ elif func == None:
+ if (callable(name)):
+ # register.object()
+ return inner(name)
+ else:
+ # @register.object('somename') or @register.object(name='somename')
+ def dec(func):
+ return self.object(name, func)
+ return dec
+ elif name != None and func != None:
+ # register.object('somename', somefunc)
+ self.jinja2_globals[name] = func
+ return func
+ else:
+ raise InvalidTemplateLibrary("Unsupported arguments to "
+ "Library.object: (%r, %r)", (name, func))
+
+ def tag(self, name_or_node=None, compile_function=None):
+ """Register a Django template tag (1) or Jinja 2 extension (2).
+
+ For (1), supports the same invocation syntax as the original
+ Django version, including use as a decorator.
+
+ For (2), since Jinja 2 extensions are classes (which can't be
+ decorated), and have the tag name effectively built in, only the
+ following syntax is supported:
+
+ register.tag(MyJinjaExtensionNode)
+ """
+ if isinstance(name_or_node, Jinja2Extension):
+ if compile_function:
+ raise InvalidTemplateLibrary('"compile_function" argument not supported for Jinja2 extensions')
+ self.jinja2_extensions.append(name_or_node)
+ return name_or_node
+ else:
+ return super(Library, self).tag(name_or_node, compile_function)
+
+ def tag_function(self, func_or_node):
+ if issubclass(func_or_node, Jinja2Extension):
+ self.jinja2_extensions.append(func_or_node)
+ return func_or_node
+ else:
+ return super(Library, self).tag_function(func_or_node)
+
+ def filter(self, name=None, filter_func=None, jinja2_only=False):
+ """Register a filter with both the Django and Jinja2 template
+ engines, if possible - or only Jinja2, if ``jinja2_only`` is
+ specified. ``jinja2_only`` does not affect conversion of the
+ filter if neccessary.
+
+ Implements a compatibility layer to handle the different
+ auto-escaping approaches transparently. Extended Jinja2 filter
+ features like environment- and contextfilters are however not
+ supported in Django. Such filters will only be registered with
+ Jinja.
+
+ Supports the same invocation syntax as the original Django
+ version, including use as a decorator.
+
+ If the function is supposed to return the registered filter
+ (by example of the superclass implementation), but has
+ registered multiple filters, a tuple of all filters is
+ returned.
+ """
+ def filter_function(f):
+ return self._register_filter(
+ getattr(f, "_decorated_function", f).__name__,
+ f, jinja2_only=jinja2_only)
+ if name == None and filter_func == None:
+ # @register.filter()
+ return filter_function
+ elif filter_func == None:
+ if (callable(name)):
+ # @register.filter
+ return filter_function(name)
+ else:
+ # @register.filter('somename') or @register.filter(name='somename')
+ def dec(func):
+ return self.filter(name, func, jinja2_only=jinja2_only)
+ return dec
+ elif name != None and filter_func != None:
+ # register.filter('somename', somefunc)
+ return self._register_filter(name, filter_func,
+ jinja2_only=jinja2_only)
+ else:
+ raise InvalidTemplateLibrary("Unsupported arguments to "
+ "Library.filter: (%r, %r)", (name, filter_func))
+
+ def _register_filter(self, name, func, jinja2_only=None):
+ filter_type, can_be_ported = guess_filter_type(func)
+ if filter_type == JINJA2 and not can_be_ported:
+ self.jinja2_filters[name] = func
+ return func
+ elif filter_type == DJANGO and not can_be_ported:
+ if jinja2_only:
+ raise ValueError('This filter cannot be ported to Jinja2.')
+ self.filters[name] = func
+ return func
+ elif jinja2_only:
+ func = django_filter_to_jinja2(func)
+ self.jinja2_filters[name] = func
+ return func
+ else:
+ # register the filter with both engines
+ django_func = jinja2_filter_to_django(func)
+ jinja2_func = django_filter_to_jinja2(func)
+ self.filters[name] = django_func
+ self.jinja2_filters[name] = jinja2_func
+ return (django_func, jinja2_func)
diff --git a/coffin/template/loader.py b/coffin/template/loader.py
new file mode 100644
index 00000000..1f2bbb1f
--- /dev/null
+++ b/coffin/template/loader.py
@@ -0,0 +1,66 @@
+"""Replacement for ``django.template.loader`` that uses Jinja 2.
+
+The module provides a generic way to load templates from an arbitrary
+backend storage (e.g. filesystem, database).
+"""
+
+from coffin.template import Template as CoffinTemplate
+from jinja2 import TemplateNotFound
+
+
+def find_template_source(name, dirs=None):
+ # This is Django's most basic loading function through which
+ # all template retrievals go. Not sure if Jinja 2 publishes
+ # an equivalent, but no matter, it mostly for internal use
+ # anyway - developers will want to start with
+ # ``get_template()`` or ``get_template_from_string`` anyway.
+ raise NotImplementedError()
+
+
+def get_template(template_name):
+ # Jinja will handle this for us, and env also initializes
+ # the loader backends the first time it is called.
+ from coffin.common import env
+ return env.get_template(template_name)
+
+
+def get_template_from_string(source):
+ """
+ Does not support then ``name`` and ``origin`` parameters from
+ the Django version.
+ """
+ from coffin.common import env
+ return env.from_string(source)
+
+
+def render_to_string(template_name, dictionary=None, context_instance=None):
+ """Loads the given ``template_name`` and renders it with the given
+ dictionary as context. The ``template_name`` may be a string to load
+ a single template using ``get_template``, or it may be a tuple to use
+ ``select_template`` to find one of the templates in the list.
+
+ ``dictionary`` may also be Django ``Context`` object.
+
+ Returns a string.
+ """
+ dictionary = dictionary or {}
+ if isinstance(template_name, (list, tuple)):
+ template = select_template(template_name)
+ else:
+ template = get_template(template_name)
+ if context_instance:
+ context_instance.update(dictionary)
+ else:
+ context_instance = dictionary
+ return template.render(context_instance)
+
+
+def select_template(template_name_list):
+ "Given a list of template names, returns the first that can be loaded."
+ for template_name in template_name_list:
+ try:
+ return get_template(template_name)
+ except TemplateNotFound:
+ continue
+ # If we get here, none of the templates could be loaded
+ raise TemplateNotFound(', '.join(template_name_list))
diff --git a/coffin/template/loaders.py b/coffin/template/loaders.py
new file mode 100644
index 00000000..cb42fd5d
--- /dev/null
+++ b/coffin/template/loaders.py
@@ -0,0 +1,38 @@
+from jinja2 import loaders
+
+
+def jinja_loader_from_django_loader(django_loader):
+ """Attempts to make a conversion from the given Django loader to an
+ similarly-behaving Jinja loader.
+
+ :param django_loader: Django loader module string.
+ :return: The similarly-behaving Jinja loader, or None if a similar loader
+ could not be found.
+ """
+ for substr, func in _JINJA_LOADER_BY_DJANGO_SUBSTR.iteritems():
+ if substr in django_loader:
+ return func()
+ return None
+
+
+def _make_jinja_app_loader():
+ """Makes an 'app loader' for Jinja which acts like
+ :mod:`django.template.loaders.app_directories`.
+ """
+ from django.template.loaders.app_directories import app_template_dirs
+ return loaders.FileSystemLoader(app_template_dirs)
+
+
+def _make_jinja_filesystem_loader():
+ """Makes a 'filesystem loader' for Jinja which acts like
+ :mod:`django.template.loaders.filesystem`.
+ """
+ from django.conf import settings
+ return loaders.FileSystemLoader(settings.TEMPLATE_DIRS)
+
+
+# Determine loaders from Django's conf.
+_JINJA_LOADER_BY_DJANGO_SUBSTR = { # {substr: callable, ...}
+ 'app_directories': _make_jinja_app_loader,
+ 'filesystem': _make_jinja_filesystem_loader,
+}
diff --git a/coffin/views/__init__.py b/coffin/views/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/views/__init__.py
diff --git a/coffin/views/defaults.py b/coffin/views/defaults.py
new file mode 100644
index 00000000..4386a49b
--- /dev/null
+++ b/coffin/views/defaults.py
@@ -0,0 +1,35 @@
+from django import http
+from django.template import Context, RequestContext
+from coffin.template.loader import render_to_string
+
+
+__all__ = ('page_not_found', 'server_error', 'shortcut')
+
+
+# no Jinja version for this needed
+from django.views.defaults import shortcut
+
+
+def page_not_found(request, template_name='404.html'):
+ """
+ Default 404 handler.
+
+ Templates: `404.html`
+ Context:
+ request_path
+ The path of the requested URL (e.g., '/app/pages/bad_page/')
+ """
+ content = render_to_string(template_name,
+ RequestContext(request, {'request_path': request.path}))
+ return http.HttpResponseNotFound(content)
+
+
+def server_error(request, template_name='500.html'):
+ """
+ 500 error handler.
+
+ Templates: `500.html`
+ Context: None
+ """
+ content = render_to_string(template_name, Context({}))
+ return http.HttpResponseServerError(content)
diff --git a/coffin/views/generic/._simple.py b/coffin/views/generic/._simple.py
new file mode 100644
index 00000000..a1bb18de
--- /dev/null
+++ b/coffin/views/generic/._simple.py
Binary files differ
diff --git a/coffin/views/generic/__init__.py b/coffin/views/generic/__init__.py
new file mode 100644
index 00000000..7ea80e4c
--- /dev/null
+++ b/coffin/views/generic/__init__.py
@@ -0,0 +1 @@
+from django.views.generic import *
diff --git a/coffin/views/generic/simple.py b/coffin/views/generic/simple.py
new file mode 100644
index 00000000..ff7678b2
--- /dev/null
+++ b/coffin/views/generic/simple.py
@@ -0,0 +1,6 @@
+import inspect
+
+from django.views.generic.simple import *
+from coffin.template import loader, RequestContext
+
+exec inspect.getsource(direct_to_template)