diff options
author | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2010-09-26 17:44:57 -0400 |
---|---|---|
committer | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2010-09-26 17:44:57 -0400 |
commit | 35954df3196e46653b6b66ea80fe95dfacc7484d (patch) | |
tree | f3949a46037f2f162639b3bbe604013491ba06dc | |
parent | 98360b7821240c21ec9b7adde155e4bcaf7bfdd5 (diff) | |
download | askbot-35954df3196e46653b6b66ea80fe95dfacc7484d.tar.gz askbot-35954df3196e46653b6b66ea80fe95dfacc7484d.tar.bz2 askbot-35954df3196e46653b6b66ea80fe95dfacc7484d.zip |
stated working on switch to jinja
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)">×</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'>●</span> + <span class="badgecount">{{user.gold}}</span> + {% endif %} + {% if user.silver %} + <span class='badge2'>●</span> + <span class="badgecount">{{user.silver}}</span> + {% endif %} + {% if user.bronze %} + <span class='badge3'>●</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'>●</span> + <span class="badgecount">{{user.gold}}</span> + {% endif %} + {% if user.silver %} + <span class='badge2'>●</span> + <span class="badgecount">{{user.silver}}</span> + {% endif %} + {% if user.bronze %} + <span class='badge3'>●</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 %}"> + « {% 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 %} »</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&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">× {{ 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"/> + <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"/> + <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 %} /> + <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 Binary files differnew file mode 100644 index 00000000..b44b5fae --- /dev/null +++ b/coffin/.___init__.py diff --git a/coffin/._common.py b/coffin/._common.py Binary files differnew file mode 100644 index 00000000..c83780af --- /dev/null +++ b/coffin/._common.py diff --git a/coffin/._interop.py b/coffin/._interop.py Binary files differnew file mode 100644 index 00000000..94121d1f --- /dev/null +++ b/coffin/._interop.py 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 Binary files differnew file mode 100644 index 00000000..c6b47018 --- /dev/null +++ b/coffin/conf/.___init__.py 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 Binary files differnew file mode 100644 index 00000000..c21832ed --- /dev/null +++ b/coffin/shortcuts/.___init__.py 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 Binary files differnew file mode 100644 index 00000000..87947898 --- /dev/null +++ b/coffin/template/.___init__.py diff --git a/coffin/template/._defaultfilters.py b/coffin/template/._defaultfilters.py Binary files differnew file mode 100644 index 00000000..8fd7d746 --- /dev/null +++ b/coffin/template/._defaultfilters.py diff --git a/coffin/template/._defaulttags.py b/coffin/template/._defaulttags.py Binary files differnew file mode 100644 index 00000000..db9dcec7 --- /dev/null +++ b/coffin/template/._defaulttags.py diff --git a/coffin/template/._library.py b/coffin/template/._library.py Binary files differnew file mode 100644 index 00000000..18fe8260 --- /dev/null +++ b/coffin/template/._library.py diff --git a/coffin/template/._loader.py b/coffin/template/._loader.py Binary files differnew file mode 100644 index 00000000..3b699201 --- /dev/null +++ b/coffin/template/._loader.py diff --git a/coffin/template/._loaders.py b/coffin/template/._loaders.py Binary files differnew file mode 100644 index 00000000..e17a06d5 --- /dev/null +++ b/coffin/template/._loaders.py 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 Binary files differnew file mode 100644 index 00000000..a1bb18de --- /dev/null +++ b/coffin/views/generic/._simple.py 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) |