summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2011-02-01 14:46:22 -0500
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2011-02-01 14:46:22 -0500
commit89b8f047899b2821296dc6e7289d6bb5c6aa059f (patch)
treec5adcea9da6e1d97d87160369124cf529561448d
parent83eae49326617638c01edca77c2adabbe3456b39 (diff)
parentcc38fb000063574555fe895f26b5ddf9b5ae23bb (diff)
downloadaskbot-89b8f047899b2821296dc6e7289d6bb5c6aa059f.tar.gz
askbot-89b8f047899b2821296dc6e7289d6bb5c6aa059f.tar.bz2
askbot-89b8f047899b2821296dc6e7289d6bb5c6aa059f.zip
merged the master branch
-rw-r--r--askbot/__init__.py2
-rw-r--r--askbot/conf/forum_data_rules.py15
-rw-r--r--askbot/conf/markup.py10
-rw-r--r--askbot/doc/source/management-commands.rst8
-rw-r--r--askbot/forms.py27
-rw-r--r--askbot/management/__init__.py82
-rw-r--r--askbot/management/commands/delete_unused_tags.py8
-rw-r--r--askbot/management/commands/fix_inbox_counts.py46
-rw-r--r--askbot/management/commands/fix_question_tags.py78
-rw-r--r--askbot/models/question.py8
-rw-r--r--askbot/models/signals.py63
-rwxr-xr-xaskbot/skins/README2
-rwxr-xr-xaskbot/skins/common/media/README1
-rw-r--r--askbot/skins/default/media/js/post.js2
-rw-r--r--askbot/skins/default/templates/faq.html2
-rw-r--r--askbot/skins/loaders.py79
-rw-r--r--askbot/skins/utils.py11
-rw-r--r--askbot/tests/__init__.py1
-rw-r--r--askbot/tests/form_tests.py35
-rw-r--r--askbot/tests/page_load_tests.py5
-rw-r--r--askbot/utils/console.py12
-rw-r--r--askbot/views/commands.py12
-rw-r--r--askbot/views/meta.py31
-rw-r--r--askbot/views/readers.py25
-rw-r--r--askbot/views/users.py47
-rw-r--r--askbot/views/writers.py28
26 files changed, 504 insertions, 136 deletions
diff --git a/askbot/__init__.py b/askbot/__init__.py
index 95b32800..b084cb5b 100644
--- a/askbot/__init__.py
+++ b/askbot/__init__.py
@@ -19,4 +19,4 @@ def get_version():
"""returns version of the askbot app
this version is meaningful for pypi only
"""
- return '0.6.63'
+ return '0.6.66'
diff --git a/askbot/conf/forum_data_rules.py b/askbot/conf/forum_data_rules.py
index 972beff8..4ce3a239 100644
--- a/askbot/conf/forum_data_rules.py
+++ b/askbot/conf/forum_data_rules.py
@@ -30,6 +30,21 @@ settings.register(
)
settings.register(
+ livesettings.BooleanValue(
+ FORUM_DATA_RULES,
+ 'FORCE_LOWERCASE_TAGS',
+ default = False,
+ description = _('Force lowercase the tags'),
+ help_text = _(
+ 'Attention: after checking this, please back up the database, '
+ 'and run a management command: '
+ '<code>python manage.py fix_question_tags</code> to globally '
+ 'rename the tags'
+ )
+ )
+)
+
+settings.register(
livesettings.IntegerValue(
FORUM_DATA_RULES,
'MAX_COMMENTS_TO_SHOW',
diff --git a/askbot/conf/markup.py b/askbot/conf/markup.py
index 23a2dc67..026c5536 100644
--- a/askbot/conf/markup.py
+++ b/askbot/conf/markup.py
@@ -15,13 +15,6 @@ MARKUP = ConfigurationGroup(
_('Markup formatting')
)
-mathjax_dir = os.path.join(
- askbot.get_install_directory(),
- 'skins',
- 'common',
- 'media'
- )
-
settings.register(
BooleanValue(
MARKUP,
@@ -47,10 +40,9 @@ settings.register(
help_text=_(
'If you enable this feature, '
'<a href="%(url)s">mathjax</a> must be '
- 'installed in directory %(dir)s'
+ 'installed on your server in its own directory.'
) % {
'url': const.DEPENDENCY_URLS['mathjax'],
- 'dir': mathjax_dir,
},
default = False
)
diff --git a/askbot/doc/source/management-commands.rst b/askbot/doc/source/management-commands.rst
index 42458f8d..188654bf 100644
--- a/askbot/doc/source/management-commands.rst
+++ b/askbot/doc/source/management-commands.rst
@@ -112,8 +112,16 @@ The commands from this section will help fix those issues.
+--------------------------------+-------------------------------------------------------------+
| `fix_answer_counts` | recalculates answer counts for all questions |
+--------------------------------+-------------------------------------------------------------+
+| `fix_inbox_counts` | recalculates response counts in the user inboxes |
++--------------------------------+-------------------------------------------------------------+
| `fix_revisionless_posts` | adds a revision record to posts that lack them |
+--------------------------------+-------------------------------------------------------------+
+| `fix_question_tags` | takes tag names from the record on the question table |
+| | and stores them in the tag table. This defect may show when |
+| | the server process is interrupted after the question was |
+| | saved, but tags were not updated, and the symptom is that |
+| | the question cannot be found via the tag search. |
++--------------------------------+-------------------------------------------------------------+
The above commands are safe to run at any time, also they do not require
additional parameters. In the future all these will be replaced with just one simple command.
diff --git a/askbot/forms.py b/askbot/forms.py
index fb9dcc73..ccb65ab5 100644
--- a/askbot/forms.py
+++ b/askbot/forms.py
@@ -94,7 +94,7 @@ class TagNamesField(forms.CharField):
split_re = re.compile(const.TAG_SPLIT_REGEX)
tag_strings = split_re.split(data)
- out_tag_list = []
+ entered_tags = []
tag_count = len(tag_strings)
if tag_count > askbot_settings.MAX_TAGS_PER_POST:
max_tags = askbot_settings.MAX_TAGS_PER_POST
@@ -118,9 +118,28 @@ class TagNamesField(forms.CharField):
if not tagname_re.search(tag):
raise forms.ValidationError(_('use-these-chars-in-tags'))
#only keep unique tags
- if tag not in out_tag_list:
- out_tag_list.append(tag)
- return u' '.join(out_tag_list)
+ if tag not in entered_tags:
+ entered_tags.append(tag)
+
+ #normalize character case of tags
+ if askbot_settings.FORCE_LOWERCASE_TAGS:
+ entered_tags = set([name.lower() for name in entered_tags])
+ else:
+ #make names of tags in the input to agree with the database
+
+ cleaned_entered_tags = set()
+ for entered_tag in entered_tags:
+ try:
+ #looks like we have to load tags one-by one
+ stored_tag = models.Tag.objects.get(
+ name__iexact = entered_tag
+ )
+ cleaned_entered_tags.add(stored_tag.name)
+ except models.Tag.DoesNotExist:
+ cleaned_entered_tags.add(entered_tag)
+ entered_tags = list(cleaned_entered_tags)
+
+ return u' '.join(entered_tags)
class WikiField(forms.BooleanField):
def __init__(self, *args, **kwargs):
diff --git a/askbot/management/__init__.py b/askbot/management/__init__.py
index e69de29b..1e2b2aaf 100644
--- a/askbot/management/__init__.py
+++ b/askbot/management/__init__.py
@@ -0,0 +1,82 @@
+import sys
+from django.core.management.base import NoArgsCommand
+from django.db import transaction
+from askbot.models import signals
+from askbot.utils import console
+
+FORMAT_STRING = '%6.2f%%'#to print progress in percent
+
+class NoArgsJob(NoArgsCommand):
+ """Base class for a job command -
+ the one that runs the same operation on
+ sets of items - each item operation in its own
+ transaction and prints progress in % of items
+ completed
+
+ The subclass must implement __init__() method
+ where self.batches data structure must be defined as follows
+ (#the whole thing is a tuple
+ {#batch is described by a dictionary
+ 'title': <string>,
+ 'query_set': <query set for the items>,
+ 'function': <function or callable that performs
+ an operation on a single item
+ and returns True if item was changed
+ False otherwise
+ item is given as argument
+ >,
+ 'items_changed_message': <string with one %d placeholder>,
+ 'nothing_changed_message': <string>
+ },
+ #more batch descriptions
+ )
+ """
+ batches = ()
+
+ def handle_noargs(self, **options):
+ """handler function that removes all signal listeners
+ then runs the job and finally restores the listerers
+ """
+ signal_data = signals.pop_all_db_signal_receivers()
+ self.run_command(**options)
+ signals.set_all_db_signal_receivers(signal_data)
+
+ def run_command(self, **options):
+ """runs the batches"""
+ for batch in self.batches:
+ self.run_batch(batch)
+
+ @transaction.commit_manually
+ def run_batch(self, batch):
+ """runs the single batch
+ prints batch title
+ then loops through the query set
+ and prints progress in %
+ afterwards there will be a short summary
+ """
+
+ sys.stdout.write(batch['title'])
+ changed_count = 0
+ checked_count = 0
+ total_count = batch['query_set'].count()
+
+ if total_count == 0:
+ return
+
+ for item in batch['query_set'].all():
+
+ item_changed = batch['function'](item)
+ transaction.commit()
+
+ if item_changed:
+ changed_count += 1
+ checked_count += 1
+
+ progress = 100*float(checked_count)/float(total_count)
+ console.print_progress(FORMAT_STRING, progress)
+ print FORMAT_STRING % 100
+
+ if changed_count:
+ print batch['changed_count_message'] % changed_count
+ else:
+ print batch['nothing_changed_message']
diff --git a/askbot/management/commands/delete_unused_tags.py b/askbot/management/commands/delete_unused_tags.py
index e5e340d0..acb28fa7 100644
--- a/askbot/management/commands/delete_unused_tags.py
+++ b/askbot/management/commands/delete_unused_tags.py
@@ -1,6 +1,7 @@
from django.core.management.base import NoArgsCommand
from django.db import transaction
from askbot import models
+from askbot.utils import console
import sys
class Command(NoArgsCommand):
@@ -17,10 +18,9 @@ class Command(NoArgsCommand):
tag.delete()
transaction.commit()
count += 1
- sys.stdout.write('%6.2f%%' % (100*float(count)/float(total)))
- sys.stdout.flush()
- sys.stdout.write('\b'*7)
- sys.stdout.write('\n')
+ progress = 100*float(count)/float(total)
+ console.print_progress('%6.2f%%', progress)
+ print '%6.2f%%' % 100
if deleted_tags:
found_count = len(deleted_tags)
diff --git a/askbot/management/commands/fix_inbox_counts.py b/askbot/management/commands/fix_inbox_counts.py
new file mode 100644
index 00000000..c2ffffdc
--- /dev/null
+++ b/askbot/management/commands/fix_inbox_counts.py
@@ -0,0 +1,46 @@
+from askbot.management import NoArgsJob
+from askbot import models
+from askbot import const
+
+ACTIVITY_TYPES = const.RESPONSE_ACTIVITY_TYPES_FOR_DISPLAY
+ACTIVITY_TYPES += (const.TYPE_ACTIVITY_MENTION,)
+
+def fix_inbox_counts(user):
+ old_new_count = user.new_response_count
+ old_seen_count = user.seen_response_count
+ new_new_count = models.ActivityAuditStatus.objects.filter(
+ user = user,
+ status = models.ActivityAuditStatus.STATUS_NEW,
+ activity__activity_type__in = ACTIVITY_TYPES
+ ).count()
+ new_seen_count = models.ActivityAuditStatus.objects.filter(
+ user = user,
+ status = models.ActivityAuditStatus.STATUS_SEEN,
+ activity__activity_type__in = ACTIVITY_TYPES
+ ).count()
+
+ (changed1, changed2) = (False, False)
+ if new_new_count != old_new_count:
+ user.new_response_count = new_new_count
+ changed1 = True
+ if new_seen_count != old_seen_count:
+ user.seen_response_count = new_seen_count
+ changed2 = True
+ if changed1 or changed2:
+ user.save()
+ return True
+ return False
+
+class Command(NoArgsJob):
+ """definition of the job that fixes response counts
+ destined for the user inboxes
+ """
+ def __init__(self, *args, **kwargs):
+ self.batches = ({
+ 'title': 'Checking inbox item counts for all users: ',
+ 'query_set': models.User.objects.all(),
+ 'function': fix_inbox_counts,
+ 'changed_count_message': 'Corrected records for %d users',
+ 'nothing_changed_message': 'No problems found'
+ },)
+ super(Command, self).__init__(*args, **kwargs)
diff --git a/askbot/management/commands/fix_question_tags.py b/askbot/management/commands/fix_question_tags.py
new file mode 100644
index 00000000..d036cfe6
--- /dev/null
+++ b/askbot/management/commands/fix_question_tags.py
@@ -0,0 +1,78 @@
+import sys
+from django.core.management.base import NoArgsCommand
+from django.db import transaction
+from askbot import models
+from askbot import forms
+from askbot.utils import console
+from askbot.models import signals
+from askbot.conf import settings as askbot_settings
+
+FORMAT_STRING = '%6.2f%%'
+
+class Command(NoArgsCommand):
+ def handle_noargs(self, **options):
+ signal_data = signals.pop_all_db_signal_receivers()
+ self.run_command()
+ signals.set_all_db_signal_receivers(signal_data)
+
+ @transaction.commit_manually
+ def run_command(self):
+ """method that runs the actual command"""
+ #go through tags and find character case duplicates and eliminate them
+ tagnames = models.Tag.objects.values_list('name', flat = True)
+ for name in tagnames:
+ dupes = models.Tag.objects.filter(name__iexact = name)
+ first_tag = dupes[0]
+ if dupes.count() > 1:
+ line = 'Found duplicate tags for %s:' % first_tag.name
+ for idx in xrange(1, dupes.count()):
+ print dupes[idx].name + ' ',
+ dupes[idx].delete()
+ print ''
+ if askbot_settings.FORCE_LOWERCASE_TAGS:
+ lowercased_name = first_tag.name.lower()
+ if first_tag.name != lowercased_name:
+ print 'Converting tag %s to lower case' % first_tag.name
+ first_tag.name = lowercased_name
+ first_tag.save()
+ transaction.commit()
+
+ #go through questions and fix tag records on each
+ questions = models.Question.objects.all()
+ checked_count = 0
+ found_count = 0
+ total_count = questions.count()
+ print "Searching for questions with inconsistent tag records:",
+ for question in questions:
+ tags = question.tags.all()
+ denorm_tag_set = set(question.get_tag_names())
+ norm_tag_set = set(question.tags.values_list('name', flat=True))
+ if norm_tag_set != denorm_tag_set:
+
+ if question.last_edited_by:
+ user = question.last_edited_by
+ timestamp = question.last_edited_at
+ else:
+ user = question.author
+ timestamp = question.added_at
+
+ tagnames = forms.TagNamesField().clean(question.tagnames)
+
+ question.update_tags(
+ tagnames = tagnames,
+ user = user,
+ timestamp = timestamp
+ )
+ question.tagnames = tagnames
+ question.save()
+ found_count += 1
+
+ transaction.commit()
+ checked_count += 1
+ progress = 100*float(checked_count)/float(total_count)
+ console.print_progress(FORMAT_STRING, progress)
+ print FORMAT_STRING % 100
+ if found_count:
+ print '%d problem questions found, tag records restored' % found_count
+ else:
+ print 'Did not find any problems'
diff --git a/askbot/models/question.py b/askbot/models/question.py
index 0f4a0027..03274a7f 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -114,12 +114,14 @@ class QuestionManager(models.Manager):
| models.Q(answers__text__search = search_query)
)
elif settings.DATABASE_ENGINE == 'postgresql_psycopg2':
- rank_clause = "ts_rank(question.text_search_vector, to_tsquery('%s'))";
+ rank_clause = "ts_rank(question.text_search_vector, to_tsquery(%s))";
search_query = '&'.join(search_query.split())
+ extra_params = ("'" + search_query + "'",)
extra_kwargs = {
- 'select': {'relevance': rank_clause % search_query},
+ 'select': {'relevance': rank_clause},
'where': ['text_search_vector @@ to_tsquery(%s)'],
- 'params': ["'" + search_query + "'"]
+ 'params': extra_params,
+ 'select_params': extra_params,
}
if askbot.conf.should_show_sort_by_relevance():
if sort_method == 'relevance-desc':
diff --git a/askbot/models/signals.py b/askbot/models/signals.py
index 83ea6eda..d28cd4a5 100644
--- a/askbot/models/signals.py
+++ b/askbot/models/signals.py
@@ -1,6 +1,11 @@
"""Custom django signals defined for the askbot forum application.
"""
import django.dispatch
+from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, post_syncdb
+try:
+ from django.db.models.signals import m2m_changed
+except ImportError:
+ pass
tags_updated = django.dispatch.Signal(
providing_args=['tags', 'user', 'timestamp']
@@ -26,3 +31,61 @@ post_updated = django.dispatch.Signal(
]
)
site_visited = django.dispatch.Signal(providing_args=['user', 'timestamp'])
+
+def pop_signal_receivers(signal):
+ """disables a given signal by removing listener functions
+ and returns the list
+ """
+ receivers = signal.receivers
+ signal.receivers = list()
+ return receivers
+
+def set_signal_receivers(signal, receiver_list):
+ """assigns a value of the receiver_list
+ to the signal receivers
+ """
+ signal.receivers = receiver_list
+
+def pop_all_db_signal_receivers():
+ """loops through all relevant signals
+ pops their receivers and returns a
+ dictionary where signals are keys
+ and lists of receivers are values
+ """
+ #this is the only askbot signal that is not defined here
+ #must use this to avoid a circular import
+ from askbot.models.badges import award_badges_signal
+ signals = (
+ #askbot signals
+ tags_updated,
+ edit_question_or_answer,
+ delete_question_or_answer,
+ flag_offensive,
+ user_updated,
+ user_logged_in,
+ post_updated,
+ award_badges_signal,
+ #django signals
+ pre_save,
+ post_save,
+ pre_delete,
+ post_delete,
+ post_syncdb,
+ )
+ if 'm2m_changed' in globals():
+ signals += m2m_changed
+
+ receiver_data = dict()
+ for signal in signals:
+ receiver_data[signal] = pop_signal_receivers(signal)
+
+ return receiver_data
+
+def set_all_db_signal_receivers(receiver_data):
+ """takes receiver data as an argument
+ where the argument is as returned by the
+ pop_all_db_signal_receivers() call
+ and sets the receivers back to the signals
+ """
+ for (signal, receivers) in receiver_data.items():
+ signal.receivers = receivers
diff --git a/askbot/skins/README b/askbot/skins/README
index 01ef0a9d..42a6ea25 100755
--- a/askbot/skins/README
+++ b/askbot/skins/README
@@ -1,7 +1,6 @@
this directory contains available skins
1) default - default skin with templates
-2) common - this directory is to media directory common to all or many templates
to create a new skin just create another directory under skins/
and start populating it with the directory structure as in
@@ -16,7 +15,6 @@ templates are resolved in the following way:
media is resolved with one extra option
* settings.ASKBOT_DEFAULT_SKIN
* 'default'
-* 'common'
media does not have to be composed of files named the same way as in default skin
whatever media you link to from your templates - will be in operation
diff --git a/askbot/skins/common/media/README b/askbot/skins/common/media/README
deleted file mode 100755
index 3376e754..00000000
--- a/askbot/skins/common/media/README
+++ /dev/null
@@ -1 +0,0 @@
-directory for media common to all or many templates
diff --git a/askbot/skins/default/media/js/post.js b/askbot/skins/default/media/js/post.js
index 12299a82..85600a22 100644
--- a/askbot/skins/default/media/js/post.js
+++ b/askbot/skins/default/media/js/post.js
@@ -662,7 +662,7 @@ var questionRetagger = function(){
data: { tags: getUniqueWords(tagInput.val()).join(' ') },
success: function(json) {
if (json['success'] === true){
- new_tags = getUniqueWords(tagInput.val());
+ new_tags = getUniqueWords(json['new_tags']);
oldTagsHtml = '';
cancelRetag();
drawNewTags(new_tags.join(' '));
diff --git a/askbot/skins/default/templates/faq.html b/askbot/skins/default/templates/faq.html
index 781650fc..74d5b0ef 100644
--- a/askbot/skins/default/templates/faq.html
+++ b/askbot/skins/default/templates/faq.html
@@ -4,7 +4,7 @@
{% block content %}
<h1>{% trans %}Frequently Asked Questions {% endtrans %}({% trans %}FAQ{% endtrans %})</h1>
<h2 class="first">{% trans %}What kinds of questions can I ask here?{% endtrans %}</h2>
-<p>{% trans %} Most importanly - questions should be <strong>relevant</strong> to this community.{% endtrans %}
+<p>{% trans %}Most importanly - questions should be <strong>relevant</strong> to this community.{% endtrans %}
{% trans %}Before asking the question - please make sure to use search to see whether your question has alredy been answered.{% endtrans %}
</p>
<h2>{% trans %}What questions should I avoid asking?{% endtrans %}</h2>
diff --git a/askbot/skins/loaders.py b/askbot/skins/loaders.py
index f264c546..c9a1a059 100644
--- a/askbot/skins/loaders.py
+++ b/askbot/skins/loaders.py
@@ -1,10 +1,14 @@
import os.path
from django.template.loaders import filesystem
+from django.template import RequestContext
+from django.http import HttpResponse
from django.utils import translation
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
+from jinja2.exceptions import TemplateNotFound
+from jinja2.utils import open_if_exists
from askbot.skins import utils
#module for skinning askbot
@@ -32,18 +36,51 @@ def load_template_source(name, dirs=None):
return filesystem.load_template_source(tname,dirs)
load_template_source.is_usable = True
+class SkinLoader(jinja_loaders.BaseLoader):
+ """loads template from the skin directory
+ code largely copy-pasted from the jinja2 internals
+ """
+ def get_source(self, environment, template):
+ pieces = jinja_loaders.split_template_path(template)
+ skin = askbot_settings.ASKBOT_DEFAULT_SKIN
+ skin_path = utils.get_path_to_skin(skin)
+ filename = os.path.join(skin_path, 'templates', *pieces)
+ print 'want file %s' % filename
+ f = open_if_exists(filename)
+ if f is None:
+ raise TemplateNotFound(template)
+ try:
+ contents = f.read().decode('utf-8')
+ finally:
+ f.close()
+
+ mtime = os.path.getmtime(filename)
+ def uptodate():
+ try:
+ return os.path.getmtime(filename) == mtime
+ except OSError:
+ return False
+ return contents, filename, uptodate
+
class SkinEnvironment(CoffinEnvironment):
"""Jinja template environment
that loads templates from askbot skins
"""
+ def __init__(self, *args, **kwargs):
+ """save the skin path and initialize the
+ Coffin Environment
+ """
+ self.skin = kwargs.pop('skin')
+ super(SkinEnvironment, self).__init__(*args, **kwargs)
+
def _get_loaders(self):
- """over-ridden function _get_loaders that creates
+ """this method is not used
+ over-ridden function _get_loaders that creates
the loader for the skin templates
"""
loaders = list()
- skin_name = askbot_settings.ASKBOT_DEFAULT_SKIN
- skin_dirs = utils.get_available_skins(selected = skin_name).values()
+ skin_dirs = utils.get_available_skins(selected = self.skin).values()
template_dirs = [os.path.join(skin_dir, 'templates') for skin_dir in skin_dirs]
loaders.append(jinja_loaders.FileSystemLoader(template_dirs))
@@ -57,6 +94,38 @@ class SkinEnvironment(CoffinEnvironment):
trans = translation.trans_real.translation(language_code)
self.install_gettext_translations(trans)
-
-ENV = SkinEnvironment(autoescape=False, extensions=['jinja2.ext.i18n'])
+ENV = SkinEnvironment(
+ autoescape=False,
+ extensions=['jinja2.ext.i18n'],
+ skin = 'default'
+ #loader = SkinLoader()
+ )
ENV.set_language(django_settings.LANGUAGE_CODE)
+
+def load_skins():
+ skins = dict()
+ for skin_name in utils.get_available_skins():
+ skins[skin_name] = SkinEnvironment(skin = skin_name)
+ skins[skin_name].set_language(django_settings.LANGUAGE_CODE)
+ return skins
+
+SKINS = load_skins()
+
+def get_template(template, request):
+ """retreives template for the skin
+ request variable will be used in the future to set
+ template according to the user preference or admins preference
+
+ at this point request variable is not used though
+ """
+ skin = SKINS[askbot_settings.ASKBOT_DEFAULT_SKIN]
+ return skin.get_template(template)
+
+def render_into_skin(template, data, request, mimetype = 'text/html'):
+ """in the future this function will be able to
+ switch skin depending on the site administrator/user selection
+ right now only admins can switch
+ """
+ context = RequestContext(request, data)
+ template = get_template(template, request)
+ return HttpResponse(template.render(context), mimetype = mimetype)
diff --git a/askbot/skins/utils.py b/askbot/skins/utils.py
index b244da00..a80c85ed 100644
--- a/askbot/skins/utils.py
+++ b/askbot/skins/utils.py
@@ -3,7 +3,6 @@
the lookup resolution process for templates and media works as follows:
* look up item in selected skin
* if not found look in 'default'
-* the look in 'common'
* raise an exception
"""
import os
@@ -29,11 +28,10 @@ def get_skins_from_dir(directory):
def get_available_skins(selected=None):
"""selected is a name of preferred skin
if it's None, then information about all skins will be returned
- otherwise, only data about selected, default and common skins
+ otherwise, only data about selected and default skins
will be returned
selected skin is guaranteed to be the first item in the dictionary
-
"""
skins = SortedDict()
if hasattr(django_settings, 'ASKBOT_EXTRA_SKINS_DIR'):
@@ -42,7 +40,6 @@ def get_available_skins(selected=None):
stock_dir = os.path.normpath(os.path.dirname(__file__))
stock_skins = get_skins_from_dir(stock_dir)
default_dir = stock_skins.pop('default')
- common_dir = stock_skins.pop('common')
skins.update(stock_skins)
@@ -55,9 +52,8 @@ def get_available_skins(selected=None):
assert(selected == 'default')
skins = SortedDict()
- #re-insert default and common as last two items
+ #re-insert default as a last item
skins['default'] = default_dir
- skins['common'] = common_dir
return skins
@@ -75,11 +71,10 @@ def get_skin_choices():
"""returns a tuple for use as a set of
choices in the form"""
skin_names = list(reversed(get_available_skins().keys()))
- skin_names.remove('common')
return zip(skin_names, skin_names)
def resolve_skin_for_media(media=None, preferred_skin = None):
- #see if file exists, if not, try skins 'default', then 'common'
+ #see if file exists, if not, try skin 'default'
available_skins = get_available_skins(selected = preferred_skin).items()
for skin_name, skin_dir in available_skins:
if os.path.isfile(os.path.join(skin_dir, 'media', media)):
diff --git a/askbot/tests/__init__.py b/askbot/tests/__init__.py
index d0e70240..1cb8d37b 100644
--- a/askbot/tests/__init__.py
+++ b/askbot/tests/__init__.py
@@ -7,3 +7,4 @@ from askbot.tests.skin_tests import *
from askbot.tests.badge_tests import *
from askbot.tests.management_command_tests import *
from askbot.tests.search_state_tests import *
+from askbot.tests.form_tests import *
diff --git a/askbot/tests/form_tests.py b/askbot/tests/form_tests.py
new file mode 100644
index 00000000..dfa2d628
--- /dev/null
+++ b/askbot/tests/form_tests.py
@@ -0,0 +1,35 @@
+from askbot.tests.utils import AskbotTestCase
+from askbot.conf import settings as askbot_settings
+from askbot import forms
+from askbot import models
+
+class TagNamesFieldTests(AskbotTestCase):
+
+ def setUp(self):
+ self.field = forms.TagNamesField()
+ self.user = self.create_user('user1')
+
+ def clean(self, value):
+ return self.field.clean(value).strip().split(' ')
+
+ def assert_tags_equal(self, tag_list1, tag_list2):
+ self.assertEqual(sorted(tag_list1), sorted(tag_list2))
+
+ def test_force_lowercase(self):
+ """FORCE_LOWERCASE setting is on
+ """
+ askbot_settings.update('FORCE_LOWERCASE_TAGS', True)
+ cleaned_tags = self.clean('Tag1 TAG5 tag1 tag5')
+ self.assert_tags_equal(cleaned_tags, ['tag1','tag5'])
+
+ def test_custom_case(self):
+ """FORCE_LOWERCASE setting is off
+ """
+ askbot_settings.update('FORCE_LOWERCASE_TAGS', False)
+ models.Tag(name = 'TAG1', created_by = self.user).save()
+ models.Tag(name = 'Tag2', created_by = self.user).save()
+ cleaned_tags = self.clean('tag1 taG2 TAG1 tag3 tag3')
+ self.assert_tags_equal(cleaned_tags, ['TAG1', 'Tag2', 'tag3'])
+
+
+
diff --git a/askbot/tests/page_load_tests.py b/askbot/tests/page_load_tests.py
index 33bcf933..28e1ea6b 100644
--- a/askbot/tests/page_load_tests.py
+++ b/askbot/tests/page_load_tests.py
@@ -231,6 +231,11 @@ class PageLoadTests(PageLoadTestCase):
status_code=200,
follow=True,
)
+ self.try_url(
+ 'faq',
+ template='faq.html',
+ status_code=200,
+ )
def test_non_user_urls(self):
self.proto_test_non_user_urls()
diff --git a/askbot/utils/console.py b/askbot/utils/console.py
index b11a8b4e..041fc839 100644
--- a/askbot/utils/console.py
+++ b/askbot/utils/console.py
@@ -1,5 +1,6 @@
"""functions that directly handle user input
"""
+import sys
import time
from askbot.utils import path
@@ -47,3 +48,14 @@ def open_new_file(prompt_phrase, extension = '', hint = None):
file_object = path.create_file_if_does_not_exist(file_path, print_warning = True)
return file_object
+
+def print_progress(format_string, progress):
+ """print dynamic output of progress of some
+ operation to the console and clear the output with
+ a backspace character to have the number increment
+ in-place"""
+ output = format_string % progress
+ sys.stdout.write(output)
+ sys.stdout.flush()
+ sys.stdout.write('\b' * len(output))
+
diff --git a/askbot/views/commands.py b/askbot/views/commands.py
index 4fdc18b2..e760080b 100644
--- a/askbot/views/commands.py
+++ b/askbot/views/commands.py
@@ -11,13 +11,12 @@ from django.core import exceptions
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
-from django.template import RequestContext
from askbot import models
from askbot.forms import CloseForm
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from askbot.utils.decorators import ajax_only, ajax_login_required
-from askbot.skins.loaders import ENV
+from askbot.skins.loaders import render_into_skin
from askbot import const
import logging
@@ -397,10 +396,7 @@ def close(request, id):#close question
else:
request.user.assert_can_close_question(question)
form = CloseForm()
- template = ENV.get_template('close.html')
- data = {'form': form, 'question': question}
- context = RequestContext(request, data)
- return HttpResponse(template.render(context))
+ return render_into_skin('close.html', data, request)
except exceptions.PermissionDenied, e:
request.user.message_set.create(message = unicode(e))
return HttpResponseRedirect(question.get_absolute_url())
@@ -428,9 +424,7 @@ def reopen(request, id):#re-open question
'closed_by_profile_url': closed_by_profile_url,
'closed_by_username': closed_by_username,
}
- context = RequestContext(request, data)
- template = ENV.get_template('reopen.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('reopen.html', data, request)
except exceptions.PermissionDenied, e:
request.user.message_set.create(message = unicode(e))
diff --git a/askbot/views/meta.py b/askbot/views/meta.py
index 7bd72ee6..7cc536ed 100644
--- a/askbot/views/meta.py
+++ b/askbot/views/meta.py
@@ -16,14 +16,13 @@ from askbot.utils.forms import get_next_url
from askbot.utils.mail import mail_moderators
from askbot.models import BadgeData, Award, User
from askbot.models import badges as badge_data
-from askbot.skins.loaders import ENV
+from askbot.skins.loaders import render_into_skin
from askbot.conf import settings as askbot_settings
from askbot import skins
def generic_view(request, template = None, page_class = None):
- template = ENV.get_template(template)
- context = RequestContext(request, {'page_class': page_class})
- return HttpResponse(template.render(context))
+ """this may be not necessary, since it is just a rewrite of render_into_skin"""
+ return render_into_skin(template, {'page_class': page_class}, request)
def config_variable(request, variable_name = None, mimetype = None):
"""Print value from the configuration settings
@@ -42,15 +41,13 @@ def server_error(request, template='500.html'):
return generic_view(request, template)
def faq(request):
- template = ENV.get_template('faq.html')
data = {
'gravatar_faq_url': reverse('faq') + '#gravatar',
#'send_email_key_url': reverse('send_email_key'),
'ask_question_url': reverse('ask'),
'page_class': 'meta',
}
- context = RequestContext(request, data)
- return HttpResponse(template.render(context))
+ return render_into_skin('faq.html', data, request)
def feedback(request):
data = {'page_class': 'meta'}
@@ -71,15 +68,11 @@ def feedback(request):
form = FeedbackForm(initial={'next':get_next_url(request)})
data['form'] = form
- context = RequestContext(request, data)
- template = ENV.get_template('feedback.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('feedback.html', data, request)
feedback.CANCEL_MESSAGE=_('We look forward to hearing your feedback! Please, give it next time :)')
def privacy(request):
- context = RequestContext(request, {'page_class': 'meta'})
- template = ENV.get_template('privacy.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('privacy.html', {'page_class': 'meta'}, request)
def logout(request):#refactor/change behavior?
#currently you click logout and you get
@@ -93,9 +86,7 @@ def logout(request):#refactor/change behavior?
'next' : get_next_url(request),
'page_class': 'meta',
}
- context = RequestContext(request, data)
- template = ENV.get_template('logout.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('logout.html', data, request)
def badges(request):#user status/reputation system
#todo: supplement database data with the stuff from badges.py
@@ -110,7 +101,6 @@ def badges(request):#user status/reputation system
).distinct()
#my_badges.query.group_by = ['badge_id']
- template = ENV.get_template('badges.html')
data = {
'active_tab': 'badges',
'badges' : badges,
@@ -118,8 +108,7 @@ def badges(request):#user status/reputation system
'mybadges' : my_badges,
'feedback_faq_url' : reverse('feedback'),
}
- context = RequestContext(request, data)
- return HttpResponse(template.render(context))
+ return render_into_skin('badges.html', data, request)
def badge(request, id):
#todo: supplement database data with the stuff from badges.py
@@ -133,15 +122,13 @@ def badge(request, id):
'-last_awarded_at'
)
- template = ENV.get_template('badge.html')
data = {
'active_tab': 'badges',
'badge_recipients' : badge_recipients,
'badge' : badge,
'page_class': 'meta',
}
- context = RequestContext(request, data)
- return HttpResponse(template.render(context))
+ return render_into_skin('badge.html', data, request)
def media(request, skin, resource):
"""view that serves static media from any skin
diff --git a/askbot/views/readers.py b/askbot/views/readers.py
index a2f5584c..06786714 100644
--- a/askbot/views/readers.py
+++ b/askbot/views/readers.py
@@ -12,7 +12,7 @@ import urllib
from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect, HttpResponse, Http404
from django.core.paginator import Paginator, EmptyPage, InvalidPage
-from django.template import RequestContext, Context
+from django.template import Context
from django.utils.http import urlencode
from django.utils import simplejson
from django.utils.translation import ugettext as _
@@ -35,7 +35,7 @@ from askbot.search.state_manager import SearchState
from askbot.templatetags import extra_tags
from askbot.templatetags import extra_filters
import askbot.conf
-from askbot.skins.loaders import ENV #jinja2 template loading enviroment
+from askbot.skins.loaders import render_into_skin, get_template#jinja2 template loading enviroment
# used in index page
#todo: - take these out of const or settings
@@ -144,7 +144,7 @@ def questions(request):
}
if q_count > search_state.page_size:
- paginator_tpl = ENV.get_template('blocks/paginator.html')
+ paginator_tpl = get_template('blocks/paginator.html', request)
#todo: remove this patch on context after all templates are moved to jinja
paginator_context['base_url'] = request.path + '?sort=%s&' % search_state.sort
data = {
@@ -275,7 +275,7 @@ def questions(request):
if meta_data.get('author_name',None):
reset_method_count += 1
- template_context = RequestContext(request, {
+ template_data = {
'language_code': translation.get_language(),
'reset_method_count': reset_method_count,
'page_class': 'main-page',
@@ -296,14 +296,13 @@ def questions(request):
'show_sort_by_relevance': askbot.conf.should_show_sort_by_relevance(),
'scope': search_state.scope,
'context' : paginator_context,
- })
+ }
assert(request.is_ajax() == False)
#ajax request is handled in a separate branch above
#before = datetime.datetime.now()
- template = ENV.get_template('main_page.html')
- response = HttpResponse(template.render(template_context))
+ response = render_into_skin('main_page.html', template_data, request)
#after = datetime.datetime.now()
#print after - before
return response
@@ -360,9 +359,7 @@ def tags(request):#view showing a listing of available tags - plain list
'keywords' : stag,
'paginator_context' : paginator_context
}
- context = RequestContext(request, data)
- template = ENV.get_template('tags.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('tags.html', data, request)
def question(request, id):#refactor - long subroutine. display question body, answers and comments
"""view that displays body of the question and
@@ -559,9 +556,7 @@ def question(request, id):#refactor - long subroutine. display question body, an
}
if request.user.is_authenticated():
data['tags_autocomplete'] = _get_tags_cache_json()
- context = RequestContext(request, data)
- template = ENV.get_template('question.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('question.html', data, request)
def revisions(request, id, object_name=None):
assert(object_name in ('Question', 'Answer'))
@@ -581,9 +576,7 @@ def revisions(request, id, object_name=None):
'post': post,
'revisions': revisions,
}
- context = RequestContext(request, data)
- template = ENV.get_template('revisions.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('revisions.html', data, request)
@ajax_only
@anonymous_forbidden
diff --git a/askbot/views/users.py b/askbot/views/users.py
index e68d1446..524ea33c 100644
--- a/askbot/views/users.py
+++ b/askbot/views/users.py
@@ -16,7 +16,6 @@ from django.core.paginator import Paginator, EmptyPage, InvalidPage
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404
-from django.template import RequestContext
from django.http import HttpResponse
from django.http import HttpResponseRedirect, Http404
from django.utils.translation import ugettext as _
@@ -30,7 +29,7 @@ from askbot.conf import settings as askbot_settings
from askbot import models
from askbot import exceptions
from askbot.models.badges import award_badges_signal
-from askbot.skins.loaders import ENV
+from askbot.skins.loaders import render_into_skin
from askbot.templatetags import extra_tags
question_type = ContentType.objects.get_for_model(models.Question)
@@ -127,9 +126,7 @@ def users(request):
'tab_id' : sortby,
'paginator_context' : paginator_context
}
- template = ENV.get_template('users.html')
- context = RequestContext(request, data)
- return HttpResponse(template.render(context))
+ return render_into_skin('users.html', data, request)
def user_moderate(request, subject):
"""user subview for moderation
@@ -222,9 +219,7 @@ def user_moderate(request, subject):
'user_rep_changed': user_rep_changed,
'user_status_changed': user_status_changed
}
- context = RequestContext(request, data)
- template = ENV.get_template('user_profile/user_moderate.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('user_profile/user_moderate.html', data, request)
#non-view function
def set_new_email(user, new_email, nomessage=False):
@@ -275,9 +270,7 @@ def edit_user(request, id):
'form' : form,
'gravatar_faq_url' : reverse('faq') + '#gravatar',
}
- context = RequestContext(request, data)
- template = ENV.get_template('user_profile/user_edit.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('user_profile/user_edit.html', data, request)
def user_stats(request, user):
@@ -390,9 +383,7 @@ def user_stats(request, user):
'awarded_badge_counts': dict(awarded_badge_counts),
'total_awards' : total_awards,
}
- context = RequestContext(request, data)
- template = ENV.get_template('user_profile/user_stats.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('user_profile/user_stats.html', data, request)
def user_recent(request, user):
@@ -664,9 +655,7 @@ def user_recent(request, user):
'view_user' : user,
'activities' : activities[:const.USER_VIEW_DATA_SIZE]
}
- context = RequestContext(request, data)
- template = ENV.get_template('user_profile/user_recent.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('user_profile/user_recent.html', data, request)
@owner_or_moderator_required
def user_responses(request, user):
@@ -733,9 +722,7 @@ def user_responses(request, user):
'view_user' : user,
'responses' : response_list,
}
- context = RequestContext(request, data)
- template = ENV.get_template('user_profile/user_inbox.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('user_profile/user_inbox.html', data, request)
@owner_or_moderator_required
def user_votes(request, user):
@@ -799,9 +786,7 @@ def user_votes(request, user):
'view_user' : user,
'votes' : votes[:const.USER_VIEW_DATA_SIZE]
}
- context = RequestContext(request, data)
- template = ENV.get_template('user_profile/user_votes.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('user_profile/user_votes.html', data, request)
def user_reputation(request, user):
reputes = models.Repute.objects.filter(user=user).order_by('-reputed_at')
@@ -838,9 +823,7 @@ def user_reputation(request, user):
'reputation': reputes,
'reps': reps
}
- context = RequestContext(request, data)
- template = ENV.get_template('user_profile/user_reputation.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('user_profile/user_reputation.html', data, request)
def user_favorites(request, user):
favorited_q_id_list= models.FavoriteQuestion.objects.filter(
@@ -868,9 +851,7 @@ def user_favorites(request, user):
'favorited_myself': favorited_q_id_list,
'view_user' : user
}
- context = RequestContext(request, data)
- template = ENV.get_template('user_profile/user_favorites.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('user_profile/user_favorites.html', data, request)
@owner_or_moderator_required
def user_email_subscriptions(request, user):
@@ -901,7 +882,6 @@ def user_email_subscriptions(request, user):
tag_filter_form = forms.TagFilterSelectionForm(instance=user)
action_status = None
- template = ENV.get_template('user_profile/user_email_subscriptions.html')
data = {
'active_tab': 'users',
'page_class': 'user-profile-page',
@@ -913,8 +893,11 @@ def user_email_subscriptions(request, user):
'tag_filter_selection_form': tag_filter_form,
'action_status': action_status,
}
- context = RequestContext(request, data)
- return HttpResponse(template.render(context))
+ return render_into_skin(
+ 'user_profile/user_email_subscriptions.html',
+ data,
+ request
+ )
user_view_call_table = {
'stats': user_stats,
diff --git a/askbot/views/writers.py b/askbot/views/writers.py
index a1c2b512..2bc4d3a3 100644
--- a/askbot/views/writers.py
+++ b/askbot/views/writers.py
@@ -16,7 +16,6 @@ from django.core.files.storage import FileSystemStorage
from django.shortcuts import get_object_or_404
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404
-from django.template import RequestContext
from django.utils import simplejson
from django.utils.html import strip_tags
from django.utils.translation import ugettext as _
@@ -27,7 +26,7 @@ from django.conf import settings
from askbot.views.readers import _get_tags_cache_json
from askbot import forms
from askbot import models
-from askbot.skins.loaders import ENV
+from askbot.skins.loaders import render_into_skin
from askbot.utils.decorators import ajax_only
from askbot.utils.functions import diff_date
from askbot.templatetags import extra_filters_jinja as template_filters
@@ -180,13 +179,11 @@ def import_data(request):
else:
form = forms.DumpUploadForm()
- template = ENV.get_template('import_data.html')
data = {
'dump_upload_form': form,
'need_configuration': (not stackexchange.is_ready())
}
- context = RequestContext(request, data)
- return HttpResponse(template.render(context))
+ return render_into_skin('import_data.html', data, request)
#@login_required #actually you can post anonymously, but then must register
def ask(request):#view used to ask a new question
@@ -255,15 +252,13 @@ def ask(request):#view used to ask a new question
form.initial['title'] = query
tags = _get_tags_cache_json()
- template = ENV.get_template('ask.html')
data = {
'active_tab': 'ask',
'form' : form,
'tags' : tags,
'email_validation_faq_url':reverse('faq') + '#validate',
}
- context = RequestContext(request, data)
- return HttpResponse(template.render(context))
+ return render_into_skin('ask.html', data, request)
@login_required
def retag_question(request, id):
@@ -282,7 +277,10 @@ def retag_question(request, id):
tags = form.cleaned_data['tags']
)
if request.is_ajax():
- response_data = {'success': True}
+ response_data = {
+ 'success': True,
+ 'new_tags': question.tagnames
+ }
data = simplejson.dumps(response_data)
return HttpResponse(data, mimetype="application/json")
else:
@@ -303,9 +301,7 @@ def retag_question(request, id):
'form' : form,
'tags' : _get_tags_cache_json(),
}
- context = RequestContext(request, data)
- template = ENV.get_template('question_retag.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('question_retag.html', data, request)
except exceptions.PermissionDenied, e:
if request.is_ajax():
response_data = {
@@ -363,9 +359,7 @@ def edit_question(request, id):
'form' : form,
'tags' : _get_tags_cache_json()
}
- context = RequestContext(request, data)
- template = ENV.get_template('question_edit.html')
- return HttpResponse(template.render(context))
+ return render_into_skin('question_edit.html', data, request)
except exceptions.PermissionDenied, e:
request.user.message_set.create(message = unicode(e))
@@ -414,15 +408,13 @@ def edit_answer(request, id):
else:
revision_form = forms.RevisionForm(answer, latest_revision)
form = forms.EditAnswerForm(answer, latest_revision)
- template = ENV.get_template('answer_edit.html')
data = {
'active_tab': 'questions',
'answer': answer,
'revision_form': revision_form,
'form': form,
}
- context = RequestContext(request, data)
- return HttpResponse(template.render(context))
+ return render_into_skin('answer_edit.html', data, request)
except exceptions.PermissionDenied, e:
request.user.message_set.create(message = unicode(e))