diff options
81 files changed, 2090 insertions, 523 deletions
@@ -13,6 +13,7 @@ nbproject settings_local.py settings.py .idea +.coverage *.iml lint env diff --git a/askbot/__init__.py b/askbot/__init__.py index 3107c689..c77c33d1 100644 --- a/askbot/__init__.py +++ b/askbot/__init__.py @@ -7,13 +7,13 @@ basic actions on behalf of the forum application import os import platform -VERSION = (0, 7, 43) +VERSION = (0, 7, 44) #keys are module names used by python imports, #values - the package qualifier to use for pip REQUIREMENTS = { 'akismet': 'akismet', - 'django': 'django==1.3.1', + 'django': 'django>=1.3.1', 'jinja2': 'Jinja2', 'coffin': 'Coffin>=0.3', 'south': 'South>=0.7.1', diff --git a/askbot/conf/minimum_reputation.py b/askbot/conf/minimum_reputation.py index 152a2079..fee880ac 100644 --- a/askbot/conf/minimum_reputation.py +++ b/askbot/conf/minimum_reputation.py @@ -107,6 +107,28 @@ settings.register( settings.register( livesettings.IntegerValue( MIN_REP, + 'MIN_REP_TO_INSERT_LINK', + default=30, + description=_('Insert clickable links') + ) +) + +settings.register( + livesettings.IntegerValue( + MIN_REP, + 'MIN_REP_TO_SUGGEST_LINK', + default=10, + description=_('Insert link suggestions as plain text'), + help_text=_( + 'This value should be smaller than that for "insert clickable links". ' + 'This setting should stop link-spamming by newly registered users.' + ) + ) +) + +settings.register( + livesettings.IntegerValue( + MIN_REP, 'MIN_REP_TO_CLOSE_OWN_QUESTIONS', default=25, description=_('Close own questions'), diff --git a/askbot/conf/social_sharing.py b/askbot/conf/social_sharing.py index 9a0f8ad4..c7c29151 100644 --- a/askbot/conf/social_sharing.py +++ b/askbot/conf/social_sharing.py @@ -8,13 +8,22 @@ from django.utils.translation import ugettext as _ SOCIAL_SHARING = ConfigurationGroup( 'SOCIAL_SHARING', - _('Sharing content on social networks'), + _('Content sharing'), super_group = EXTERNAL_SERVICES ) settings.register( BooleanValue( SOCIAL_SHARING, + 'RSS_ENABLED', + default=True, + description=_('Check to enable RSS feeds') + ) +) + +settings.register( + BooleanValue( + SOCIAL_SHARING, 'ENABLE_SHARING_TWITTER', default=True, description=_('Check to enable sharing of questions on Twitter') diff --git a/askbot/const/message_keys.py b/askbot/const/message_keys.py index 74fcbf7f..291381cb 100644 --- a/askbot/const/message_keys.py +++ b/askbot/const/message_keys.py @@ -54,7 +54,4 @@ def get_i18n_message(key): 'Please contact the forum administrator to reach a resolution.' ) } - if key in messages: - return messages.get(key) - else: - raise KeyError(key) + return messages[key] diff --git a/askbot/deployment/__init__.py b/askbot/deployment/__init__.py index 8832cd01..59926cf4 100644 --- a/askbot/deployment/__init__.py +++ b/askbot/deployment/__init__.py @@ -127,12 +127,23 @@ def deploy_askbot(directory, options): path_utils.create_path(directory) - if django.VERSION[0] == 1 and django.VERSION[1] < 3: + if django.VERSION[0] > 1: + raise Exception( + 'Django framework with major version > 1 is not supported' + ) + + if django.VERSION[1] < 3: #force people install the django-staticfiles app context['staticfiles_app'] = '' else: context['staticfiles_app'] = "'django.contrib.staticfiles'," + if django.VERSION[1] <=3: + auth_context_processor = 'django.core.context_processors.auth' + else: + auth_context_processor = 'django.contrib.auth.context_processors.auth' + context['auth_context_processor'] = auth_context_processor + path_utils.deploy_into( directory, new_project = create_new_project, diff --git a/askbot/deps/django_authopenid/views.py b/askbot/deps/django_authopenid/views.py index 05e0cdc5..9e383b49 100644 --- a/askbot/deps/django_authopenid/views.py +++ b/askbot/deps/django_authopenid/views.py @@ -41,6 +41,8 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth import authenticate from django.core.urlresolvers import reverse from django.forms.util import ErrorList +from django.shortcuts import render +from django.template.loader import get_template from django.views.decorators import csrf from django.utils.encoding import smart_unicode from django.utils.html import escape @@ -48,7 +50,6 @@ from django.utils.translation import ugettext as _ from django.utils.safestring import mark_safe from askbot.mail import send_mail from recaptcha_works.decorators import fix_recaptcha_remote_ip -from askbot.skins.loaders import render_into_skin, get_template from askbot.deps.django_authopenid.ldap_auth import ldap_create_user from askbot.deps.django_authopenid.ldap_auth import ldap_authenticate from askbot.utils.loading import load_module @@ -169,7 +170,7 @@ def logout_page(request): 'page_class': 'meta', 'have_federated_login_methods': util.have_enabled_federated_login_methods() } - return render_into_skin('authopenid/logout.html', data, request) + return render(request, 'authopenid/logout.html', data) def get_url_host(request): if request.is_secure(): @@ -721,7 +722,7 @@ def show_signin_view( data['major_login_providers'] = major_login_providers.values() data['minor_login_providers'] = minor_login_providers.values() - return render_into_skin(template_name, data, request) + return render(request, template_name, data) @login_required def delete_login_method(request): @@ -993,7 +994,7 @@ def register(request, login_provider_name=None, user_identifier=None): 'login_type':'openid', 'gravatar_faq_url':reverse('faq') + '#gravatar', } - return render_into_skin('authopenid/complete.html', data, request) + return render(request, 'authopenid/complete.html', data) def signin_failure(request, message): """ @@ -1052,7 +1053,7 @@ def verify_email_and_register(request): return HttpResponseRedirect(reverse('index')) else: data = {'page_class': 'validate-email-page'} - return render_into_skin('authopenid/verify_email.html', data, request) + return render(request, 'authopenid/verify_email.html', data) @not_authenticated @decorators.valid_password_login_provider_required @@ -1138,10 +1139,10 @@ def signup_with_password(request): 'minor_login_providers': minor_login_providers.values(), 'login_form': login_form } - return render_into_skin( + return render( + request, 'authopenid/signup_with_password.html', - context_data, - request + context_data ) #what if request is not posted? @@ -1201,7 +1202,7 @@ def send_email_key(email, key, handler_url_name='user_account_recover'): '?validation_code=' + key } template = get_template('authopenid/email_validation.html') - message = template.render(data) + message = template.render(data)#todo: inject language preference send_mail(subject, message, django_settings.DEFAULT_FROM_EMAIL, [email]) def send_user_new_email_key(user): @@ -1273,4 +1274,4 @@ def validation_email_sent(request): 'change_email_url': reverse('user_changeemail'), 'action_type': 'validate' } - return render_into_skin('authopenid/changeemail.html', data, request) + return render(request, 'authopenid/changeemail.html', data) diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index a971d9d6..ca776770 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -3,6 +3,15 @@ Changes in Askbot Development version ------------------- +* Feedback sender's email is added to the Reply-To header + in the feedback form (Evgeny) + +0.7.44 (Nov 11, 2012) +------------------- +* Support for django 1.4 (Adolfo) +* Added option to enable/disable rss feeds (Evgeny) +* Added minimum reputation to insert links and hotlinked images (Evgeny) +* Added minimum reputation to suggest links as plain text (Evgeny) * Added support of Haystack for search (Adolfo) * Added minimum reputation setting to accept any answer as correct (Evgeny) * Added "VIP" option to groups - if checked, all posts belong to the group and users of that group in the future will be able to moderate those posts. Moderation features for VIP group are in progress (Evgeny) diff --git a/askbot/doc/source/contributors.rst b/askbot/doc/source/contributors.rst index a795d84e..71d942bb 100644 --- a/askbot/doc/source/contributors.rst +++ b/askbot/doc/source/contributors.rst @@ -6,8 +6,8 @@ This is the list of contributors to the code of Askbot project. The list is probably incomplete, apologies for any omissions. Thanks for all your help -Programming and documentation ------------------------------ +Programming, bug fixes and documentation +---------------------------------------- * Mike Chen & Sailing Cai - original authors of CNPROG forum * Evgeny Fadeev - founder of askbot * `Adolfo Fitoria <http://fitoria.net>`_ @@ -42,6 +42,7 @@ Programming and documentation * `Alexandros <https://github.com/alexandros-z>`_ * `Paul Backhouse <https://github.com/powlo>`_ * `jtrain <https://github.com/jtrain>`_ +* Niki Rocco Translations ------------ diff --git a/askbot/feed.py b/askbot/feed.py index 285bb452..0e1102b1 100644 --- a/askbot/feed.py +++ b/askbot/feed.py @@ -12,12 +12,14 @@ """ #!/usr/bin/env python #encoding:utf-8 +from django.contrib.syndication.views import Feed + import itertools -from django.contrib.syndication.feeds import Feed from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext as _ from django.core.exceptions import ObjectDoesNotExist +from django.http import Http404 from askbot.models import Post from askbot.conf import settings as askbot_settings @@ -36,10 +38,12 @@ class RssIndividualQuestionFeed(Feed): def description(self): return askbot_settings.APP_DESCRIPTION - def get_object(self, bits): - if len(bits) != 1: - raise ObjectDoesNotExist - return Post.objects.get_questions().get(id__exact = bits[0]) + def get_object(self, request, pk): + if askbot_settings.RSS_ENABLED is False: + raise Http404 + #hack to get the request object into the Feed class + self.request = request + return Post.objects.get_questions().get(id__exact = pk) def item_link(self, item): """get full url to the item @@ -67,6 +71,7 @@ class RssIndividualQuestionFeed(Feed): ) answers = Post.objects.get_answers().filter(thread = item.thread) + for answer in answers: chain_elements.append([answer,]) chain_elements.append( @@ -134,7 +139,7 @@ class RssLastestQuestionsFeed(Feed): """returns url without the slug because the slug can change """ - return askbot_settings.APP_URL + item.get_absolute_url(no_slug = True) + return self.link() + item.get_absolute_url(no_slug = True) def item_description(self, item): """returns the description for the item @@ -144,6 +149,8 @@ class RssLastestQuestionsFeed(Feed): def items(self, item): """get questions for the feed """ + if askbot_settings.RSS_ENABLED is False: + raise Http404 #initial filtering qs = Post.objects.get_questions().filter(deleted=False) @@ -164,7 +171,10 @@ class RssLastestQuestionsFeed(Feed): return qs.order_by('-thread__last_activity_at')[:30] - + #hack to get the request object into the Feed class + def get_feed(self, obj, request): + self.request = request + return super(RssLastestQuestionsFeed, self).get_feed(obj, request) def main(): """main function for use as a script diff --git a/askbot/forms.py b/askbot/forms.py index 1c5bc3d6..ed47e20e 100644 --- a/askbot/forms.py +++ b/askbot/forms.py @@ -4,6 +4,7 @@ import re from django import forms from askbot import const from askbot.const import message_keys +from django.core.exceptions import PermissionDenied from django.forms.util import ErrorList from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy, string_concat @@ -273,6 +274,11 @@ class EditorField(forms.CharField): min_length = 10 # sentinel default value def __init__(self, *args, **kwargs): + user = kwargs.pop('user', None) + if user is None: + raise ValueError('user parameter is required') + self.user = user + editor_attrs = kwargs.pop('editor_attrs', {}) super(EditorField, self).__init__(*args, **kwargs) self.required = True @@ -295,6 +301,17 @@ class EditorField(forms.CharField): self.min_length ) % self.min_length raise forms.ValidationError(msg) + + if self.user.is_anonymous(): + #we postpone this validation if user is posting + #before logging in, up until publishing the post + return value + + try: + self.user.assert_can_post_text(value) + except PermissionDenied, e: + raise forms.ValidationError(unicode(e)) + return value @@ -302,7 +319,10 @@ class QuestionEditorField(EditorField): """Editor field for the questions""" def __init__(self, *args, **kwargs): - super(QuestionEditorField, self).__init__(*args, **kwargs) + user = kwargs.pop('user', None) + super(QuestionEditorField, self).__init__( + user=user, *args, **kwargs + ) self.length_error_template_singular = \ 'question body must be > %d character' self.length_error_template_plural = \ @@ -477,10 +497,12 @@ class EditorForm(forms.Form): the field must be created dynamically, so it's added in the __init__() function""" - def __init__(self, editor_attrs=None): + def __init__(self, user=None, editor_attrs=None): super(EditorForm, self).__init__() editor_attrs = editor_attrs or {} - self.fields['editor'] = EditorField(editor_attrs=editor_attrs) + self.fields['editor'] = EditorField( + user=user, editor_attrs=editor_attrs + ) class DumpUploadForm(forms.Form): @@ -775,6 +797,7 @@ class PostPrivatelyForm(forms.Form, FormWithHideableFields): def allows_post_privately(self): user = self._user return ( + askbot_settings.GROUPS_ENABLED and \ user and user.is_authenticated() and \ user.can_make_group_private_posts() ) @@ -897,9 +920,10 @@ class AskForm(PostAsSomeoneForm, PostPrivatelyForm): ) def __init__(self, *args, **kwargs): + user = kwargs.pop('user', None) super(AskForm, self).__init__(*args, **kwargs) #it's important that this field is set up dynamically - self.fields['text'] = QuestionEditorField() + self.fields['text'] = QuestionEditorField(user=user) #hide ask_anonymously field if askbot_settings.ALLOW_ASK_ANONYMOUSLY is False: self.hide_field('ask_anonymously') @@ -932,11 +956,12 @@ class AskWidgetForm(forms.Form, FormWithHideableFields): ) def __init__(self, include_text=True, *args, **kwargs): + user = kwargs.pop('user', None) super(AskWidgetForm, self).__init__(*args, **kwargs) #hide ask_anonymously field if not askbot_settings.ALLOW_ASK_ANONYMOUSLY: self.hide_field('ask_anonymously') - self.fields['text'] = QuestionEditorField() + self.fields['text'] = QuestionEditorField(user=user) if not include_text: self.hide_field('text') #hack to make it validate @@ -1020,7 +1045,11 @@ class AskByEmailForm(forms.Form): 'required': ASK_BY_EMAIL_SUBJECT_HELP } ) - body_text = QuestionEditorField() + + def __init__(self, *args, **kwargs): + user = kwargs.pop('user', None) + super(AskByEmailForm, self).__init__(*args, **kwargs) + self.fields['body_text'] = QuestionEditorField(user=user) def clean_sender(self): """Cleans the :attr:`~askbot.forms.AskByEmail.sender` attribute @@ -1074,7 +1103,6 @@ class AskByEmailForm(forms.Form): class AnswerForm(PostAsSomeoneForm, PostPrivatelyForm): - text = AnswerEditorField() wiki = WikiField() openid = forms.CharField( required=False, max_length=255, @@ -1084,7 +1112,7 @@ class AnswerForm(PostAsSomeoneForm, PostPrivatelyForm): def __init__(self, *args, **kwargs): super(AnswerForm, self).__init__(*args, **kwargs) - self.fields['text'] = AnswerEditorField() + self.fields['text'] = AnswerEditorField(user=kwargs['user']) self.fields['email_notify'].widget.attrs['id'] = \ 'question-subscribe-updates' @@ -1169,11 +1197,11 @@ class EditQuestionForm(PostAsSomeoneForm, PostPrivatelyForm): def __init__(self, *args, **kwargs): """populate EditQuestionForm with initial data""" self.question = kwargs.pop('question') - self.user = kwargs['user']#preserve for superclass + self.user = kwargs.pop('user')#preserve for superclass revision = kwargs.pop('revision') super(EditQuestionForm, self).__init__(*args, **kwargs) #it is important to add this field dynamically - self.fields['text'] = QuestionEditorField() + self.fields['text'] = QuestionEditorField(user=self.user) self.fields['title'].initial = revision.title self.fields['text'].initial = revision.text self.fields['tags'].initial = revision.tagnames @@ -1275,9 +1303,10 @@ class EditAnswerForm(PostAsSomeoneForm, PostPrivatelyForm): def __init__(self, answer, revision, *args, **kwargs): self.answer = answer + user = kwargs.pop('user', None) super(EditAnswerForm, self).__init__(*args, **kwargs) #it is important to add this field dynamically - self.fields['text'] = AnswerEditorField() + self.fields['text'] = AnswerEditorField(user=user) self.fields['text'].initial = revision.text self.fields['wiki'].initial = answer.wiki diff --git a/askbot/importers/stackexchange/management/commands/load_stackexchange.py b/askbot/importers/stackexchange/management/commands/load_stackexchange.py index ff17a523..a021b62d 100644 --- a/askbot/importers/stackexchange/management/commands/load_stackexchange.py +++ b/askbot/importers/stackexchange/management/commands/load_stackexchange.py @@ -1,11 +1,12 @@ #todo: http://stackoverflow.com/questions/837828/how-to-use-a-slug-in-django -DEBUGME = False +DEBUGME = False import os import re import sys from unidecode import unidecode import zipfile from datetime import datetime +from django.conf import settings as django_settings from django.core.management.base import BaseCommand, CommandError import askbot.importers.stackexchange.parse_models as se_parser from xml.etree import ElementTree as et @@ -17,11 +18,18 @@ import askbot.deps.django_authopenid.models as askbot_openid import askbot.importers.stackexchange.models as se from askbot.forms import EditUserEmailFeedsForm from askbot.conf import settings as askbot_settings -from django.contrib.auth.models import Message as DjangoMessage + +try: + from django.contrib.auth.models import Message as DjangoMessage +except ImportError: + from askbot.models.message import Message as DjangoMessage + from django.utils.translation import ugettext as _ +from askbot.utils.console import ProgressBar from askbot.utils.slug import slugify from askbot.models.badges import award_badges_signal, award_badges from askbot.importers.stackexchange.management import is_ready as importer_is_ready +from optparse import make_option #from markdown2 import Markdown #markdowner = Markdown(html4tags=True) @@ -30,7 +38,8 @@ if DEBUGME == True: from askbot.utils import dummy_transaction as transaction HEAP = hpy() else: - from django.db import transaction + #from django.db import transaction + from askbot.utils import dummy_transaction as transaction xml_read_order = ( 'VoteTypes','UserTypes','Users','Users2Votes', @@ -154,12 +163,12 @@ class X(object):# #or use database to store these associations try: if isinstance(se_post, se.PostComment): - return askbot.Comment.objects.get(id=COMMENT[se_post.id].id) + return askbot.Post.objects.get(id=COMMENT[se_post.id].id) post_type = se_post.post_type.name if post_type == 'Question': - return askbot.Question.objects.get(id=QUESTION[se_post.id].id) + return askbot.Post.objects.get(id=QUESTION[se_post.id].id) elif post_type == 'Answer': - return askbot.Answer.objects.get(id=ANSWER[se_post.id].id) + return askbot.Post.objects.get(id=ANSWER[se_post.id].id) else: raise Exception('unknown post type %s' % post_type) except KeyError: @@ -280,12 +289,46 @@ class X(object):# return slugify(cls.badge_exceptions.get(name, name).lower()) class Command(BaseCommand): - help = 'Loads StackExchange data from unzipped directory of XML files into the ASKBOT database' + help = """Loads StackExchange data from SE dump .zip file +it may be helpful to split this procedure in two:\n +* read the dump (with option --read-se-dump) +* transfer data to askbot (with option --process-data) +""" args = 'se_dump_dir' + option_list = BaseCommand.option_list + ( + make_option('-r', '--read-dump', + action='store_true', + dest='read_dump', + default=False, + help='Only read the the dump' + ), + make_option('-p', '--process-data', + action='store_true', + dest='process_data', + default=False, + help='Only process the data, assuming that the dump is loaded' + ) + ) + @transaction.commit_manually def handle(self, *arg, **kwarg): + if django_settings.DEBUG: + raise CommandError( + 'Please set DEBUG to False in the settings.py to reduce ' + 'RAM usage during the import process' + ) + + #process the command line arguments, if given + if kwarg['read_dump'] is False and kwarg['process_data'] is False: + #make them both true as a hack to simulate a condition where + #no flags selected means the same as both are indeed selected + kwarg['read_dump'] = True + kwarg['process_data'] = True + + askbot_settings.update('LIMIT_ONE_ANSWER_PER_USER', False) + if not importer_is_ready(): raise CommandError( "Looks like stackexchange tables are not yet initialized in the database.\n" @@ -299,16 +342,23 @@ class Command(BaseCommand): if len(arg) < 1 or not os.path.isfile(arg[0]): raise CommandError('Error: first argument must be a zip file with the SE forum data') - self.zipfile = self.open_dump(arg[0]) - #read the data into SE tables - for item in xml_read_order: - time_before = datetime.now() - self.load_xml_file(item) - transaction.commit() - time_after = datetime.now() - if DEBUGME == True: - print time_after - time_before - print HEAP.heap() + if kwarg['read_dump']: + self.zipfile = self.open_dump(arg[0]) + #read the data into SE tables + for item in xml_read_order: + time_before = datetime.now() + self.load_xml_file(item) + transaction.commit() + time_after = datetime.now() + if DEBUGME == True: + print time_after - time_before + print HEAP.heap() + + if kwarg['process_data'] is False: + #that means we just wanted to load the xml dump to + #do the second step in another go in order to have + #more ram for the transfer of data from SE to Askbot databases + return #this is important so that when we clean up messages #automatically generated by the procedures below @@ -320,36 +370,36 @@ class Command(BaseCommand): self.save_askbot_message_id_list() #transfer data into ASKBOT tables - print 'Transferring users...', + print 'Transferring users...' self.transfer_users() transaction.commit() print 'done.' - print 'Transferring content edits...', + print 'Transferring content edits...' sys.stdout.flush() self.transfer_question_and_answer_activity() transaction.commit() print 'done.' - print 'Transferring view counts...', + print 'Transferring view counts...' sys.stdout.flush() self.transfer_question_view_counts() transaction.commit() print 'done.' - print 'Transferring comments...', + print 'Transferring comments...' sys.stdout.flush() self.transfer_comments() transaction.commit() print 'done.' - print 'Transferring badges and badge awards...', + print 'Transferring badges and badge awards...' sys.stdout.flush() self.transfer_badges() transaction.commit() print 'done.' - print 'Transferring Q&A votes...', + print 'Transferring Q&A votes...' sys.stdout.flush() self.transfer_QA_votes()#includes favorites, accepts and flags transaction.commit() print 'done.' - print 'Transferring comment votes...', + print 'Transferring comment votes...' sys.stdout.flush() self.transfer_comment_votes() transaction.commit() @@ -396,7 +446,8 @@ class Command(BaseCommand): """transfers some messages from SE to ASKBOT """ - for m in se.Message.objects.all().iterator(): + messages = se.Message.objects.all() + for m in ProgressBar(messages.iterator(), messages.count()): if m.is_read: continue if m.user is None: @@ -522,10 +573,7 @@ class Command(BaseCommand): def mark_activity(self,p,u,t): """p,u,t - post, user, timestamp """ - if isinstance(p, askbot.Question): - p.thread.set_last_activity(last_activity_by=u, last_activity_at=t) - elif isinstance(p, askbot.Answer): - p.question.thread.set_last_activity(last_activity_by=u, last_activity_at=t) + p.thread.set_last_activity(last_activity_by=u, last_activity_at=t) def _process_post_rollback_revision_group(self, rev_group): #todo: don't know what to do here as there were no @@ -647,7 +695,9 @@ class Command(BaseCommand): c_group = [] #this loop groups revisions by revision id, then calls process function #for the revision grup (elementary revisions posted at once) - for se_rev in se_revs.iterator(): + message = 'Processing revisions' + count = se_revs.count() + for se_rev in ProgressBar(se_revs.iterator(), count, message): if se_rev.revision_guid == c_guid: c_group.append(se_rev) else: @@ -660,7 +710,8 @@ class Command(BaseCommand): self._process_post_revision_group(c_group) def transfer_comments(self): - for se_c in se.PostComment.objects.all().iterator(): + comments = se.PostComment.objects.all() + for se_c in ProgressBar(comments.iterator(), comments.count()): if se_c.deletion_date: print 'Warning deleted comment %d dropped' % se_c.id sys.stdout.flush() @@ -683,7 +734,9 @@ class Command(BaseCommand): def _collect_missing_badges(self): self._missing_badges = {} - for se_b in se.Badge.objects.all(): + badges = se.Badge.objects.all() + message = 'Collecting missing badges' + for se_b in ProgressBar(badges.iterator(), badges.count(), message): name = X.get_badge_name(se_b.name) try: #todo: query badge from askbot.models.badges @@ -700,7 +753,9 @@ class Command(BaseCommand): def _award_badges(self): #note: SE does not keep information on #content-related badges like askbot does - for se_a in se.User2Badge.objects.all().iterator(): + badges = se.User2Badge.objects.all() + message = 'Awarding badges' + for se_a in ProgressBar(badges.iterator(), badges.count(), message): if se_a.user.id == -1: continue #skip community user u = USER[se_a.user.id] @@ -743,7 +798,8 @@ class Command(BaseCommand): pass def transfer_question_view_counts(self): - for se_q in se.Post.objects.filter(post_type__name='Question').iterator(): + questions = se.Post.objects.filter(post_type__name='Question') + for se_q in ProgressBar(questions.iterator(), questions.count()): q = X.get_post(se_q) if q is None: continue @@ -752,7 +808,8 @@ class Command(BaseCommand): def transfer_QA_votes(self): - for v in se.Post2Vote.objects.all().iterator(): + votes = se.Post2Vote.objects.all() + for v in ProgressBar(votes.iterator(), votes.count()): vote_type = v.vote_type.name if not vote_type in X.vote_actions: continue @@ -779,7 +836,8 @@ class Command(BaseCommand): transaction.commit() def transfer_comment_votes(self): - for v in se.Comment2Vote.objects.all().iterator(): + votes = se.Comment2Vote.objects.all() + for v in ProgressBar(votes.iterator(), votes.count()): vote_type = v.vote_type.name if vote_type not in ('UpMod', 'Offensive'): continue @@ -822,10 +880,11 @@ class Command(BaseCommand): xml_data = self.zipfile.read(xml_path) tree = et.fromstring(xml_data) - print 'loading from %s to %s' % (xml_path, table_name) , + print 'loading from %s to %s' % (xml_path, table_name) model = models.get_model('stackexchange', table_name) i = 0 - for row in tree.findall('.//row'): + rows = tree.findall('.//row') + for row in ProgressBar(iter(rows), len(rows)): model_entry = model() i += 1 for col in row.getchildren(): @@ -849,7 +908,8 @@ class Command(BaseCommand): return xml_file_basename + '.xml' def transfer_users(self): - for se_u in se.User.objects.all().iterator(): + se_users = se.User.objects.all() + for se_u in ProgressBar(se_users.iterator(), se_users.count()): #if se_u.id == -1:#skip the Community user # continue u = askbot.User() diff --git a/askbot/locale/ca/LC_MESSAGES/djangojs.po b/askbot/locale/ca/LC_MESSAGES/djangojs.po index f64685b0..e66e382a 100644 --- a/askbot/locale/ca/LC_MESSAGES/djangojs.po +++ b/askbot/locale/ca/LC_MESSAGES/djangojs.po @@ -308,7 +308,7 @@ msgstr "pujar arxiu adjunt" #~ msgid "cancel" #~ msgstr "anul·lar" -#~ msgid "confirm abandon comment" +#~ msgid "Are you sure you don't want to post this comment?" #~ msgstr "confirmar abandó del commenari" #~ msgid "confirm delete comment" diff --git a/askbot/locale/de/LC_MESSAGES/djangojs.po b/askbot/locale/de/LC_MESSAGES/djangojs.po index 9d962b9e..e7837fe5 100644 --- a/askbot/locale/de/LC_MESSAGES/djangojs.po +++ b/askbot/locale/de/LC_MESSAGES/djangojs.po @@ -282,7 +282,7 @@ msgstr "Bitte wähle und lade eine Datei hoch:" #~ msgid "enter %s more characters" #~ msgstr "please enter at least %s more characters" -#~ msgid "confirm abandon comment" +#~ msgid "Are you sure you don't want to post this comment?" #~ msgstr "Are you sure you do not want to post this comment?" #~ msgid "confirm delete comment" diff --git a/askbot/locale/en/LC_MESSAGES/djangojs.po b/askbot/locale/en/LC_MESSAGES/djangojs.po index fa93edf1..733fa92b 100644 --- a/askbot/locale/en/LC_MESSAGES/djangojs.po +++ b/askbot/locale/en/LC_MESSAGES/djangojs.po @@ -289,7 +289,7 @@ msgstr "Please choose and upload a file:" #~ msgid "enter %s more characters" #~ msgstr "please enter at least %s more characters" -#~ msgid "confirm abandon comment" +#~ msgid "Are you sure you don't want to post this comment?" #~ msgstr "Are you sure you do not want to post this comment?" #~ msgid "confirm delete comment" diff --git a/askbot/locale/es/LC_MESSAGES/djangojs.po b/askbot/locale/es/LC_MESSAGES/djangojs.po index a9528c80..ca14a402 100644 --- a/askbot/locale/es/LC_MESSAGES/djangojs.po +++ b/askbot/locale/es/LC_MESSAGES/djangojs.po @@ -317,7 +317,7 @@ msgstr "Por favor, selecciona y sube un archivo:" #~ msgid "cancel" #~ msgstr "cancelar" -#~ msgid "confirm abandon comment" +#~ msgid "Are you sure you don't want to post this comment?" #~ msgstr "seguro que no quieres enviar este comentario?" #~ msgid "confirm delete comment" diff --git a/askbot/locale/fi/LC_MESSAGES/djangojs.po b/askbot/locale/fi/LC_MESSAGES/djangojs.po index 2c4ad82c..10160a4f 100644 --- a/askbot/locale/fi/LC_MESSAGES/djangojs.po +++ b/askbot/locale/fi/LC_MESSAGES/djangojs.po @@ -319,7 +319,7 @@ msgstr "Valitse ja lataa tiedosto:" #~ msgid "cancel" #~ msgstr "peruuta" -#~ msgid "confirm abandon comment" +#~ msgid "Are you sure you don't want to post this comment?" #~ msgstr "Oletko varma, ettet halua julkaista tätä kommenttia?" #~ msgid "confirm delete comment" diff --git a/askbot/locale/fr/LC_MESSAGES/djangojs.po b/askbot/locale/fr/LC_MESSAGES/djangojs.po index 1c65f20a..b8cb521d 100644 --- a/askbot/locale/fr/LC_MESSAGES/djangojs.po +++ b/askbot/locale/fr/LC_MESSAGES/djangojs.po @@ -324,7 +324,7 @@ msgstr "Merci de choisir un fichier et de le télécharger vers le serveur:" #~ msgid "cancel" #~ msgstr "annuler" -#~ msgid "confirm abandon comment" +#~ msgid "Are you sure you don't want to post this comment?" #~ msgstr "Etes-vous sûr de ne pas vouloir publier ce commentaire?" #~ msgid "confirm delete comment" diff --git a/askbot/locale/hi/LC_MESSAGES/djangojs.po b/askbot/locale/hi/LC_MESSAGES/djangojs.po index d2c85414..dc6957be 100644 --- a/askbot/locale/hi/LC_MESSAGES/djangojs.po +++ b/askbot/locale/hi/LC_MESSAGES/djangojs.po @@ -318,7 +318,7 @@ msgstr "कृपया चà¥à¤¨à¥‡à¤‚ और फ़ाइल को अपलà #~ msgid "cancel" #~ msgstr "रदà¥à¤¦ करें" -#~ msgid "confirm abandon comment" +#~ msgid "Are you sure you don't want to post this comment?" #~ msgstr "कà¥à¤¯à¤¾ आप सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ करेंगे कि आप इस टिपà¥à¤ªà¤£à¥€ को पोसà¥à¤Ÿ नहीं करना चाहते हैं?" #~ msgid "confirm delete comment" diff --git a/askbot/locale/ja/LC_MESSAGES/djangojs.po b/askbot/locale/ja/LC_MESSAGES/djangojs.po index 88cb4dd0..9d4d0e3a 100644 --- a/askbot/locale/ja/LC_MESSAGES/djangojs.po +++ b/askbot/locale/ja/LC_MESSAGES/djangojs.po @@ -315,7 +315,7 @@ msgstr "ファイルをé¸æŠžã—ã¦ã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã—ã¦ãã ã•ã„:" #~ msgid "cancel" #~ msgstr "ã‚ャンセル" -#~ msgid "confirm abandon comment" +#~ msgid "Are you sure you don't want to post this comment?" #~ msgstr "本当ã«ã“ã®ã‚³ãƒ¡ãƒ³ãƒˆã‚’投稿ã—ãŸã„ã§ã™ã‹ï¼Ÿ" #~ msgid "confirm delete comment" diff --git a/askbot/locale/pt_BR/LC_MESSAGES/djangojs.po b/askbot/locale/pt_BR/LC_MESSAGES/djangojs.po index cadb90a3..30e84f0b 100644 --- a/askbot/locale/pt_BR/LC_MESSAGES/djangojs.po +++ b/askbot/locale/pt_BR/LC_MESSAGES/djangojs.po @@ -377,7 +377,7 @@ msgstr "gravar arquivo anexo" #~ msgstr "cancelar" # 100% -#~ msgid "confirm abandon comment" +#~ msgid "Are you sure you don't want to post this comment?" #~ msgstr "confirme abandonar comentário" # 100% diff --git a/askbot/locale/ru/LC_MESSAGES/djangojs.po b/askbot/locale/ru/LC_MESSAGES/djangojs.po index 9b25feed..8e3fcb0a 100644 --- a/askbot/locale/ru/LC_MESSAGES/djangojs.po +++ b/askbot/locale/ru/LC_MESSAGES/djangojs.po @@ -346,7 +346,7 @@ msgstr "ПожалуйÑта, выберите и загрузите файл:" #~ msgid "cancel" #~ msgstr "отмена" -#~ msgid "confirm abandon comment" +#~ msgid "Are you sure you don't want to post this comment?" #~ msgstr "Ð’Ñ‹ дейÑтвительно уверены, что не хотите добавлÑÑ‚ÑŒ Ñтот комментарий?" #~ msgid "confirm delete comment" diff --git a/askbot/locale/tr/LC_MESSAGES/djangojs.po b/askbot/locale/tr/LC_MESSAGES/djangojs.po index 22373215..dc5d3765 100644 --- a/askbot/locale/tr/LC_MESSAGES/djangojs.po +++ b/askbot/locale/tr/LC_MESSAGES/djangojs.po @@ -321,7 +321,7 @@ msgstr "Yüklenecek dosyayı seçin" #~ msgid "cancel" #~ msgstr "iptal" -#~ msgid "confirm abandon comment" +#~ msgid "Are you sure you don't want to post this comment?" #~ msgstr "Yorum yazmak istemediÄŸinize emin misiniz?" #~ msgid "confirm delete comment" diff --git a/askbot/locale/zh_CN/LC_MESSAGES/djangojs.po b/askbot/locale/zh_CN/LC_MESSAGES/djangojs.po index c8775c04..23ce1708 100644 --- a/askbot/locale/zh_CN/LC_MESSAGES/djangojs.po +++ b/askbot/locale/zh_CN/LC_MESSAGES/djangojs.po @@ -337,7 +337,7 @@ msgstr "ä¸Šä¼ æ–‡ä»¶é™„ä»¶" #~ msgstr "å–消" # 100% -#~ msgid "confirm abandon comment" +#~ msgid "Are you sure you don't want to post this comment?" #~ msgstr "确认放弃评论" #~ msgid "confirm delete comment" diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index 74aa27e9..6e181b33 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -2,8 +2,17 @@ these automatically catch email-related exceptions """ import os +import re import smtplib -import logging +import sys +from askbot import exceptions +from askbot import const +from askbot.conf import settings as askbot_settings +from askbot.mail import parsing +from askbot.utils import url_utils +from askbot.utils.file_utils import store_file +from askbot.utils.html import absolutize_urls +from bs4 import BeautifulSoup from django.core import mail from django.conf import settings as django_settings from django.core.exceptions import PermissionDenied @@ -12,14 +21,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat from django.template import Context from django.utils.html import strip_tags -from askbot import exceptions -from askbot import const -from askbot.conf import settings as askbot_settings -from askbot.utils import url_utils -from askbot.utils.file_utils import store_file -from askbot.utils.html import absolutize_urls -from bs4 import BeautifulSoup #todo: maybe send_mail functions belong to models #or the future API def prefix_the_subject_line(subject): @@ -82,16 +84,17 @@ def thread_headers(post, orig_post, update): return headers def clean_html_email(email_body): - '''needs more clenup might not work for other email templates - that does not use table layout''' - - remove_linejump = lambda s: s.replace('\n', '') - + """returns the content part from an HTML email. + todo: needs more clenup might not work for other email templates + that do not use table layout + """ soup = BeautifulSoup(email_body) - table_tds = soup.find('body') - phrases = map(lambda s: s.strip(), - filter(bool, table_tds.get_text().split('\n'))) - + body_element = soup.find('body') + filter_func = lambda s: bool(s.strip()) + phrases = map( + lambda s: s.strip(), + filter(filter_func, body_element.get_text().split('\n')) + ) return '\n\n'.join(phrases) def send_mail( @@ -133,7 +136,7 @@ def send_mail( if related_object is not None: assert(activity_type is not None) except Exception, error: - logging.critical(unicode(error)) + sys.stderr.write('\n' + unicode(error).encode('utf-8') + '\n') if raise_on_failure == True: raise exceptions.EmailNotSent(unicode(error)) @@ -170,7 +173,7 @@ def mail_moderators( msg.content_subtype = 'html' msg.send() except smtplib.SMTPException, error: - logging.critical(unicode(error)) + sys.stderr.write('\n' + error.encode('utf-8') + '\n') if raise_on_failure == True: raise exceptions.EmailNotSent(unicode(error)) @@ -266,12 +269,13 @@ def bounce_email( def extract_reply(text): """take the part above the separator - and discard the last line above the separator""" - if const.REPLY_SEPARATOR_REGEX.search(text): - text = const.REPLY_SEPARATOR_REGEX.split(text)[0] - return '\n'.join(text.splitlines(True)[:-3]) - else: - return text + and discard the last line above the separator + ``text`` is the input text + """ + return parsing.extract_reply_contents( + text, + const.REPLY_SEPARATOR_REGEX + ) def process_attachment(attachment): """will save a single @@ -290,11 +294,11 @@ def process_attachment(attachment): def extract_user_signature(text, reply_code): """extracts email signature as text trailing the reply code""" - striped_text = strip_tags(text) - if reply_code in striped_text: + stripped_text = strip_tags(text) + if reply_code in stripped_text: #extract the signature tail = list() - for line in reversed(striped_text.splitlines()): + for line in reversed(stripped_text.splitlines()): #scan backwards from the end until the magic line if reply_code in line: break @@ -307,13 +311,13 @@ def extract_user_signature(text, reply_code): return '\n'.join(tail) else: - return '' + return None -def process_parts(parts, reply_code = None): - """Process parts will upload the attachments and parse out the - body, if body is multipart. Secondly - links to attachments - will be added to the body of the question. +def process_parts(parts, reply_code=None): + """Uploads the attachments and parses out the + body, if body is multipart. + Links to attachments will be added to the body of the question. Returns ready to post body of the message and the list of uploaded files. """ @@ -362,10 +366,10 @@ def process_emailed_question( 'subject': subject, 'body_text': body_text } - form = AskByEmailForm(data) + user = User.objects.get(email__iexact=from_address) + form = AskByEmailForm(data, user=user) if form.is_valid(): email_address = form.cleaned_data['email'] - user = User.objects.get(email__iexact = email_address) if user.can_post_by_email() is False: raise PermissionDenied(messages.insufficient_reputation(user)) diff --git a/askbot/mail/lamson_handlers.py b/askbot/mail/lamson_handlers.py index da09eec2..8bfa86f7 100644 --- a/askbot/mail/lamson_handlers.py +++ b/askbot/mail/lamson_handlers.py @@ -3,13 +3,13 @@ import functools from django.core.files.uploadedfile import SimpleUploadedFile from django.conf import settings as django_settings from django.template import Context +from django.template.loader import get_template from django.utils.translation import ugettext as _ from lamson.routing import route, stateless from lamson.server import Relay from askbot.models import ReplyAddress, Group, Tag from askbot import mail from askbot.conf import settings as askbot_settings -from askbot.skins.loaders import get_template #we might end up needing to use something like this #to distinguish the reply text from the quoted original message @@ -147,13 +147,6 @@ def process_reply(func): #here is the business part of this function parts = get_parts(message) - for part_type, content in parts: - if part_type == 'body': - print '===============================' - print 'message :', content - break - else: - continue func( from_address = message.From, subject_line = message['Subject'], @@ -173,7 +166,7 @@ def process_reply(func): if error is not None: template = get_template('email/reply_by_email_error.html') - body_text = template.render(Context({'error':error})) + body_text = template.render(Context({'error':error}))#todo: set lang mail.send_mail( subject_line = "Error posting your reply", body_text = body_text, @@ -250,7 +243,7 @@ def VALIDATE_EMAIL( mail.send_mail( subject_line = _('Re: Welcome to %(site_name)s') % data, - body_text = template.render(Context(data)), + body_text = template.render(Context(data)),#todo: set lang recipient_list = [from_address,] ) except ValueError: @@ -281,7 +274,7 @@ def PROCESS( #2) process body text and email signature user = reply_address_object.user - if signature:#if there, then it was stripped + if signature is not None:#if there, then it was stripped if signature != user.email_signature: user.email_signature = signature else:#try to strip signature @@ -319,6 +312,6 @@ def PROCESS( mail.send_mail( subject_line = _('Re: %s') % subject_line, - body_text = template.render(Context(data)), + body_text = template.render(Context(data)),#todo: set lang recipient_list = [from_address,] ) diff --git a/askbot/mail/messages.py b/askbot/mail/messages.py index 3ab3ff2f..0e888545 100644 --- a/askbot/mail/messages.py +++ b/askbot/mail/messages.py @@ -3,8 +3,8 @@ of email messages for various occasions """ import functools from django.template import Context +from django.template.loader import get_template from askbot.conf import settings as askbot_settings -from askbot.skins.loaders import get_template from askbot.utils import html as html_utils def message(template = None): @@ -16,7 +16,7 @@ def message(template = None): def wrapped(*args, **kwargs): template_object = get_template(template) data = func(*args, **kwargs) - return template_object.render(Context(data)) + return template_object.render(Context(data))#todo: set lang return wrapped return decorate diff --git a/askbot/mail/parsing.py b/askbot/mail/parsing.py new file mode 100644 index 00000000..31be6abb --- /dev/null +++ b/askbot/mail/parsing.py @@ -0,0 +1,85 @@ +"""a module for parsing email response text +this file is a candidate for publishing as an independent module +""" +import re +import sys + +#Regexes for quote separators +#add more via variables ending with _QUOTE_RE +#These regexes do not contain any trailing: +#* newline chars, +#* lines starting with | or > +#* lines consisting entirely of empty space +#expressions are stripped of month and day names +#to keep them simpler and make the additions of language variants +#easier. +GMAIL_QUOTE_RE = r'\nOn [^\n]* wrote:\Z' +GMAIL_SECOND_QUOTE_RE = r'\n\d{4}/\d{1,2}/\d{1,2} [^\n]*\Z' +YAHOO_QUOTE_RE = r'\n_+\n\s*From: [^\n]+\nTo: [^\n]+\nSent: [^\n]+\nSubject: [^\n]+\Z' +KMAIL_QUOTE_RE = r'\AOn [^\n]+ you wrote:\s*\n\n' +OUTLOOK_RTF_QUOTE_RE = r'\nSubject: [^\n]+\nFrom: [^\n]+\nTo: [^\n]+\nDate: [^\n]+\Z' +OUTLOOK_TEXT_QUOTE_RE = r'\n_+\Z' + +def compile_quote_regexes(): + regex_names = filter( + lambda v: v.endswith('_QUOTE_RE'), + globals().keys() + ) + compiled_regexes = list() + for regex_name in regex_names: + regex = globals()[regex_name] + compiled_regexes.append( + re.compile( + regex, + re.MULTILINE | re.IGNORECASE + ) + ) + return compiled_regexes + +CLIENT_SPECIFIC_QUOTE_REGEXES = compile_quote_regexes() + +def strip_trailing_empties_and_quotes(text): + #strip empty lines and quote lines starting with | and > + return re.sub(r'(([\n\s\xa0])|(\n[\|>][^\n]*))*\Z', '', text) + +def strip_leading_empties(text): + return re.sub(r'\A[\n\s\xa0]*', '', text) + +def strip_email_client_quote_separator(text): + """strips email client quote separator from the responses, + e.g. (on such date XYZ wrote) + + if one client-specific separator matches, then result + is immediately returned + """ + for regex in CLIENT_SPECIFIC_QUOTE_REGEXES: + if regex.search(text): + return regex.sub('', text) + #did not find a quote separator!!! log it + log_message = u'\nno matching quote separator: %s\n' % text + sys.stderr.write(log_message.encode('utf-8')) + text_lines = text.splitlines(False) + return ''.join(text_lines[:-3])#strip 3 lines as a guess + +def extract_reply_contents(text, reply_separator=None): + """If reply_separator is given, + take the part above the separator. + After, strip the email-client-specific text + + ``text`` is the input text + ``reply_separator`` is either a string or a regex object + """ + if reply_separator: + if isinstance(reply_separator, basestring): + text = text.split(reply_separator)[0] + else: + testre = re.compile('test') + if type(testre) == type(reply_separator): + text = reply_separator.split(text)[0] + else: + raise ValueError('reply_separator must be a string or a compiled regex') + + text = strip_trailing_empties_and_quotes(text) + text = strip_email_client_quote_separator(text) + text = strip_trailing_empties_and_quotes(text) + return strip_leading_empties(text) diff --git a/askbot/management/commands/send_accept_answer_reminders.py b/askbot/management/commands/send_accept_answer_reminders.py index 119d7611..2b8b0851 100644 --- a/askbot/management/commands/send_accept_answer_reminders.py +++ b/askbot/management/commands/send_accept_answer_reminders.py @@ -1,6 +1,7 @@ import datetime from django.core.management.base import NoArgsCommand from django.conf import settings as django_settings +from django.template.loader import get_template from askbot import models from askbot import const from askbot.conf import settings as askbot_settings @@ -8,7 +9,6 @@ from django.utils.translation import ugettext as _ from django.utils.translation import ungettext from askbot import mail from askbot.utils.classes import ReminderSchedule -from askbot.skins.loaders import get_template from django.template import Context DEBUG_THIS_COMMAND = False @@ -73,7 +73,7 @@ class Command(NoArgsCommand): } template = get_template('email/accept_answer_reminder.html') - body_text = template.render(Context(data)) + body_text = template.render(Context(data))#todo: set lang if DEBUG_THIS_COMMAND: print "User: %s<br>\nSubject:%s<br>\nText: %s<br>\n" % \ diff --git a/askbot/management/commands/send_unanswered_question_reminders.py b/askbot/management/commands/send_unanswered_question_reminders.py index 3fa390ad..982dbafb 100644 --- a/askbot/management/commands/send_unanswered_question_reminders.py +++ b/askbot/management/commands/send_unanswered_question_reminders.py @@ -1,4 +1,5 @@ from django.core.management.base import NoArgsCommand +from django.template.loader import get_template from askbot import models from askbot import const from askbot.conf import settings as askbot_settings @@ -6,7 +7,6 @@ from django.utils.translation import ungettext from askbot import mail from askbot.utils.classes import ReminderSchedule from askbot.models.question import Thread -from askbot.skins.loaders import get_template from django.template import Context DEBUG_THIS_COMMAND = False @@ -78,7 +78,7 @@ class Command(NoArgsCommand): } template = get_template('email/unanswered_question_reminder.html') - body_text = template.render(Context(data)) + body_text = template.render(Context(data))#todo: set lang if DEBUG_THIS_COMMAND: diff --git a/askbot/media/js/post.js b/askbot/media/js/post.js index 3ed61182..fb6f6ef0 100644 --- a/askbot/media/js/post.js +++ b/askbot/media/js/post.js @@ -1591,7 +1591,7 @@ EditCommentForm.prototype.reset = function(){ EditCommentForm.prototype.confirmAbandon = function(){ this.focus(true); this._textarea.addClass('highlight'); - var answer = confirm(gettext('confirm abandon comment')); + var answer = confirm(gettext("Are you sure you don't want to post this comment?")); this._textarea.removeClass('highlight'); return answer; }; @@ -1635,6 +1635,7 @@ EditCommentForm.prototype.getSaveHandler = function(){ dataType: "json", data: post_data, success: function(json) { + //type is 'edit' or 'add' if (me._type == 'add'){ me._comment.dispose(); me._comment.getContainerWidget().reRenderComments(json); diff --git a/askbot/media/js/tinymce/plugins/preview/preview.html b/askbot/media/js/tinymce/plugins/preview/preview.html index 67e7b142..16693edb 100644 --- a/askbot/media/js/tinymce/plugins/preview/preview.html +++ b/askbot/media/js/tinymce/plugins/preview/preview.html @@ -1,6 +1,7 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
+<link type="text/css" href="/m/default/media/style/style.css" rel="stylesheet" />
<script type="text/javascript" src="../../tiny_mce_popup.js"></script>
<script type="text/javascript" src="jscripts/embed.js"></script>
<script type="text/javascript"><!--
diff --git a/askbot/media/style/style.css b/askbot/media/style/style.css index 79bc422b..ea81746f 100644 --- a/askbot/media/style/style.css +++ b/askbot/media/style/style.css @@ -120,13 +120,9 @@ pre { font-family: Consolas, Monaco, Liberation Mono, Lucida Console, Monospace; font-size: 100%; margin-bottom: 10px; - /*overflow: auto;*/ - background-color: #F5F5F5; padding-left: 5px; padding-top: 5px; - /*width: 671px;*/ - padding-bottom: 20px ! ie7; } code { @@ -1797,6 +1793,7 @@ ul#related-tags li { width: 100%; } .wmd-preview { + color: #525252; margin: 0; padding: 5px; background-color: #F5F5F5; @@ -1819,9 +1816,16 @@ ul#related-tags li { .wmd-preview blockquote { background-color: #eee; } -.wmd-preview IMG { +.wmd-preview img { max-width: 600px; } +.wmd-preview a { + color: #1b79bd; +} +.wmd-preview li { + margin-bottom: 7px; + font-size: 14px; +} .defaultSkin table.mceLayout, .defaultSkin table.mceLayout tr.mceFirst td { border: none; @@ -3748,14 +3752,6 @@ p.signup_p { .simple-subscribe-options input { display: inline; } -/* a workaround to set link colors correctly */ -.wmd-preview a { - color: #1b79bd; -} -.wmd-preview li { - margin-bottom: 7px; - font-size: 14px; -} .search-result-summary { font-weight: bold; font-size: 18px; @@ -4146,3 +4142,39 @@ textarea.tipped-input { width: 515px; margin-bottom: 0px; } +/* modifications for small screens */ +@media screen and (max-width: 960px) { + /* content margins touch viewport */ +} +@media screen and (max-width: 480px) { + .content-wrapper { + width: 100%; + } + #ContentRight { + display: none; + } + #ContentLeft { + width: 100%; + } + #secondaryHeader #scopeWrapper { + display: none; + } + #askButton { + display: block; + float: none; + margin: auto; + margin-top: 5px; + } + .main-page h1, + .main-page .counts .views, + .main-page .counts .votes, + .main-page .rss, + .main-page .tabBar, + .main-page .tags, + .main-page .userinfo { + display: none; + } + .short-summary { + width: 100%; + } +} diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less index 9e2e2a25..69f4daf7 100644 --- a/askbot/media/style/style.less +++ b/askbot/media/style/style.less @@ -102,18 +102,15 @@ pre { font-family: Consolas, Monaco, Liberation Mono, Lucida Console, Monospace; font-size: 100%; margin-bottom: 10px; - /*overflow: auto;*/ background-color: #F5F5F5; padding-left: 5px; padding-top: 5px; - /*width: 671px;*/ padding-bottom: 20px ! ie7; } code { font-family: Consolas, Monaco, Liberation Mono, Lucida Console, Monospace; font-size: 100%; - } blockquote { @@ -407,7 +404,7 @@ body.user-messages { display:block; float:left; } - + #homeButton:hover{ .sprites(-6px-45,-36px); } @@ -428,6 +425,7 @@ body.user-messages { line-height:55px; margin-left:16px } + .on{ background:url(../images/scopearrow.png) no-repeat center bottom; } @@ -533,7 +531,7 @@ body.anon { #searchBar { width: 500px; .searchInput { - width: 440px; + width: 435px; } .searchInputCancelable { @@ -1678,15 +1676,16 @@ ul#related-tags li { } .wmd-preview { + color: #525252; margin: 0; padding: 5px; background-color: #F5F5F5; min-height: 20px; overflow: auto; font-size:13px; - font-family:@body-font; + font-family: @body-font; - p{ + p { margin-bottom: 14px; line-height: 1.4; font-size: 14px; @@ -1695,19 +1694,27 @@ ul#related-tags li { p:last-child{ margin-bottom: 0; } -} -.wmd-preview pre { - background-color: #E7F1F8; + pre { + background-color: #E7F1F8; + } -} + blockquote { + background-color: #eee; + } -.wmd-preview blockquote { - background-color: #eee; -} + img { + max-width: 600px; + } + + a { + color: @link; + } -.wmd-preview IMG { - max-width: 600px; + li { + margin-bottom:7px; + font-size:14px; + } } .defaultSkin table.mceLayout, @@ -3605,17 +3612,6 @@ p.signup_p { } } -/* a workaround to set link colors correctly */ - -.wmd-preview a { - color:@link; -} - -.wmd-preview li { - margin-bottom:7px; - font-size:14px; -} - .search-result-summary { font-weight: bold; font-size:18px; @@ -4008,3 +4004,43 @@ textarea.tipped-input { width: 515px; margin-bottom: 0px; } + +/* modifications for small screens */ +@media screen and (max-width: 960px) {/* content margins touch viewport */ +} +@media screen and (max-width: 480px) { + .content-wrapper { + width: 100%; + } + #ContentRight { + display: none; + } + #ContentLeft { + width: 100%; + } + #secondaryHeader #scopeWrapper { + display: none; + } + #askButton { + display: block; + float: none; + margin: auto; + margin-top: 5px; + } + #homeButton { + } + .main-page { + h1, + .counts .views, + .counts .votes, + .rss, + .tabBar, + .tags, + .userinfo { + display: none; + } + } + .short-summary { + width: 100%; + } +} diff --git a/askbot/media/style/tinymce/content.css b/askbot/media/style/tinymce/content.css index 41c45d1e..7a81caaf 100644 --- a/askbot/media/style/tinymce/content.css +++ b/askbot/media/style/tinymce/content.css @@ -51,8 +51,78 @@ font[face=mceinline] {font-family:inherit !important} /* overrides on top of default */ body.mceContentBody { - font-size: 13px; - background-color:#fff; - padding: 0; + font-size: 14px; + background-color: white; + padding: 5px; + margin: 0; line-height:135%; + color: #525252; + font-family: Arial; +} + +body.mceContentBody p { + margin-bottom: 14px; + line-height: 1.4; + font-size: 14px; +} + +body.mceContentBody p:last-child { + margin-bottom: 0; +} + +body.mceContentBody pre { + background-color: #E7F1F8; +} + +body.mceContentBody blockquote { + background-color: #eee; +} + +body.mceContentBody img { + max-width: 600px; +} + +body.mceContentBody a { + color: #1b79bd; +} + +body.mceContentBody li { + margin-bottom:7px; + font-size:14px; +} + +body.mceContentBody ul { + list-style: disc; + margin-left: 20px; + padding-left: 0px; + margin-bottom: 1em; +} + +body.mceContentBody ol { + list-style: decimal; + margin-left: 30px; + margin-bottom: 1em; + padding-left: 0px; +} + +body.mceContentBody pre { + font-family: Consolas, Monaco, Liberation Mono, Lucida Console, Monospace; + font-size: 100%; + margin-bottom: 10px; + background-color: #F5F5F5; + padding-left: 5px; + padding-top: 5px; + padding-bottom: 20px ! ie7; +} + +body.mceContentBody code { + font-family: Consolas, Monaco, Liberation Mono, Lucida Console, Monospace; + font-size: 100%; +} + +body.mceContentBody blockquote { + margin-bottom: 10px; + margin-right: 15px; + padding: 10px 0px 1px 10px; + background-color: #F5F5F5; } diff --git a/askbot/middleware/forum_mode.py b/askbot/middleware/forum_mode.py index d593a6f2..331fe85b 100644 --- a/askbot/middleware/forum_mode.py +++ b/askbot/middleware/forum_mode.py @@ -11,7 +11,7 @@ from askbot.conf import settings as askbot_settings PROTECTED_VIEW_MODULES = ( 'askbot.views', - 'django.contrib.syndication.views', + 'askbot.feed', ) ALLOWED_VIEWS = ( 'askbot.views.meta.media', @@ -30,7 +30,13 @@ def is_view_allowed(func): """True, if view is allowed to access by the special rule """ - view_path = func.__module__ + '.' + func.__name__ + if hasattr(func, '__name__'): + view_path = func.__module__ + '.' + func.__name__ + elif hasattr(func, '__class__'): + view_path = func.__module__ + '.' + func.__class__.__name__ + else: + view_path = '' + return view_path in ALLOWED_VIEWS class ForumModeMiddleware(object): @@ -51,7 +57,7 @@ class ForumModeMiddleware(object): if is_view_allowed(resolver_match.func): return - + if is_view_protected(resolver_match.func): request.user.message_set.create( _('Please log in to use %s') % \ diff --git a/askbot/migrations/0155_remove_unused_internal_tags.py b/askbot/migrations/0155_remove_unused_internal_tags.py new file mode 100644 index 00000000..d4ef662f --- /dev/null +++ b/askbot/migrations/0155_remove_unused_internal_tags.py @@ -0,0 +1,387 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + print 'Deleting unused tags added for internal bookkeeping...' + tags = orm['askbot.Tag'].objects.filter(name__startswith='_internal_') + count = tags.count() + if count > 0: + tags.delete() + print '%d tags formatted _internal_X' % count + else: + print 'None found' + + + def backwards(self, orm): + "Write your backwards methods here." + + + models = { + 'askbot.activity': { + 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"}, + 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}), + 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': "orm['auth.User']"}), + 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'askbot.activityauditstatus': { + 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'}, + 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'askbot.anonymousanswer': { + 'Meta': {'object_name': 'AnonymousAnswer'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Post']"}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'askbot.anonymousquestion': { + 'Meta': {'object_name': 'AnonymousQuestion'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), + 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}), + 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'askbot.askwidget': { + 'Meta': {'object_name': 'AskWidget'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'include_text_field': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'inner_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'outer_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Tag']", 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'askbot.award': { + 'Meta': {'object_name': 'Award', 'db_table': "u'award'"}, + 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"}) + }, + 'askbot.badgedata': { + 'Meta': {'ordering': "('slug',)", 'object_name': 'BadgeData'}, + 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.Award']", 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}) + }, + 'askbot.draftanswer': { + 'Meta': {'object_name': 'DraftAnswer'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['askbot.Thread']"}) + }, + 'askbot.draftquestion': { + 'Meta': {'object_name': 'DraftQuestion'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125', 'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '300', 'null': 'True'}) + }, + 'askbot.emailfeedsetting': { + 'Meta': {'unique_together': "(('subscriber', 'feed_type'),)", 'object_name': 'EmailFeedSetting'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"}) + }, + 'askbot.favoritequestion': { + 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"}) + }, + 'askbot.group': { + 'Meta': {'object_name': 'Group', '_ormbases': ['auth.Group']}, + 'description': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'described_group'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}), + 'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}), + 'is_vip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}), + 'moderate_answers_to_enquirers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'openness': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}), + 'preapproved_email_domains': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'preapproved_emails': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}) + }, + 'askbot.groupmembership': { + 'Meta': {'object_name': 'GroupMembership', '_ormbases': ['auth.AuthUserGroups']}, + 'authusergroups_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.AuthUserGroups']", 'unique': 'True', 'primary_key': 'True'}), + 'level': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}) + }, + 'askbot.markedtag': { + 'Meta': {'object_name': 'MarkedTag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"}) + }, + 'askbot.post': { + 'Meta': {'object_name': 'Post'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}), + 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_posts'", 'null': 'True', 'to': "orm['auth.User']"}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_posts'", 'symmetrical': 'False', 'through': "orm['askbot.PostToGroup']", 'to': "orm['askbot.Group']"}), + 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_posts'", 'null': 'True', 'to': "orm['auth.User']"}), + 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': "orm['auth.User']"}), + 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}), + 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'summary': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'posts'", 'null': 'True', 'blank': 'True', 'to': "orm['askbot.Thread']"}), + 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'askbot.postflagreason': { + 'Meta': {'object_name': 'PostFlagReason'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'details': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post_reject_reasons'", 'to': "orm['askbot.Post']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'askbot.postrevision': { + 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'}, + 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'approved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'approved_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}), + 'by_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'revised_at': ('django.db.models.fields.DateTimeField', [], {}), + 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '125', 'blank': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'}) + }, + 'askbot.posttogroup': { + 'Meta': {'unique_together': "(('post', 'group'),)", 'object_name': 'PostToGroup', 'db_table': "'askbot_post_groups'"}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']"}) + }, + 'askbot.questionview': { + 'Meta': {'object_name': 'QuestionView'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Post']"}), + 'when': ('django.db.models.fields.DateTimeField', [], {}), + 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"}) + }, + 'askbot.questionwidget': { + 'Meta': {'object_name': 'QuestionWidget'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order_by': ('django.db.models.fields.CharField', [], {'default': "'-added_at'", 'max_length': '18'}), + 'question_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '7'}), + 'search_query': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'style': ('django.db.models.fields.TextField', [], {'default': '"\\n@import url(\'http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700\');\\nbody {\\n overflow: hidden;\\n}\\n\\n#container {\\n width: 200px;\\n height: 350px;\\n}\\nul {\\n list-style: none;\\n padding: 5px;\\n margin: 5px;\\n}\\nli {\\n border-bottom: #CCC 1px solid;\\n padding-bottom: 5px;\\n padding-top: 5px;\\n}\\nli:last-child {\\n border: none;\\n}\\na {\\n text-decoration: none;\\n color: #464646;\\n font-family: \'Yanone Kaffeesatz\', sans-serif;\\n font-size: 15px;\\n}\\n"', 'blank': 'True'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'askbot.replyaddress': { + 'Meta': {'object_name': 'ReplyAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}), + 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reply_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'reply_action': ('django.db.models.fields.CharField', [], {'default': "'auto_answer_or_comment'", 'max_length': '32'}), + 'response_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'askbot.repute': { + 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"}, + 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}), + 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'askbot.tag': { + 'Meta': {'ordering': "('-used_count', 'name')", 'object_name': 'Tag', 'db_table': "u'tag'"}, + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'suggested_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'suggested_tags'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'tag_wiki': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'described_tag'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}), + 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'askbot.thread': { + 'Meta': {'object_name': 'Thread'}, + 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.FavoriteQuestion']", 'to': "orm['auth.User']"}), + 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_threads'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_threads'", 'symmetrical': 'False', 'through': "orm['askbot.ThreadToGroup']", 'to': "orm['askbot.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': "orm['auth.User']"}), + 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}), + 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'askbot.threadtogroup': { + 'Meta': {'unique_together': "(('thread', 'group'),)", 'object_name': 'ThreadToGroup', 'db_table': "'askbot_thread_groups'"}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}), + 'visibility': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}) + }, + 'askbot.vote': { + 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}), + 'vote': ('django.db.models.fields.SmallIntegerField', [], {}), + 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"}) + }, + 'auth.authusergroups': { + 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'AuthUserGroups', 'db_table': "'auth_user_groups'", 'managed': 'False'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['askbot'] + symmetrical = True diff --git a/askbot/migrations/0156_add_message_model_in_new_django.py b/askbot/migrations/0156_add_message_model_in_new_django.py new file mode 100644 index 00000000..950196fa --- /dev/null +++ b/askbot/migrations/0156_add_message_model_in_new_django.py @@ -0,0 +1,386 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models, connection +import django + +def db_table_exists(table_name): + return table_name in connection.introspection.table_names() + +class Migration(SchemaMigration): + + def forwards(self, orm): + if django.get_version() > '1.3.1' and not db_table_exists('auth_message'): + db.create_table('auth_message', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='_message_set', to=orm['auth.User'])), + ('message', self.gf('django.db.models.fields.TextField')()) + )) + db.send_create_signal('askbot', ['Message']) + + def backwards(self, orm): + pass + + models = { + 'askbot.activity': { + 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"}, + 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}), + 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': "orm['auth.User']"}), + 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'askbot.activityauditstatus': { + 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'}, + 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'askbot.anonymousanswer': { + 'Meta': {'object_name': 'AnonymousAnswer'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Post']"}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'askbot.anonymousquestion': { + 'Meta': {'object_name': 'AnonymousQuestion'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), + 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}), + 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'askbot.askwidget': { + 'Meta': {'object_name': 'AskWidget'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'include_text_field': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'inner_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'outer_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Tag']", 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'askbot.award': { + 'Meta': {'object_name': 'Award', 'db_table': "u'award'"}, + 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"}) + }, + 'askbot.badgedata': { + 'Meta': {'ordering': "('slug',)", 'object_name': 'BadgeData'}, + 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.Award']", 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}) + }, + 'askbot.draftanswer': { + 'Meta': {'object_name': 'DraftAnswer'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['askbot.Thread']"}) + }, + 'askbot.draftquestion': { + 'Meta': {'object_name': 'DraftQuestion'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125', 'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '300', 'null': 'True'}) + }, + 'askbot.emailfeedsetting': { + 'Meta': {'unique_together': "(('subscriber', 'feed_type'),)", 'object_name': 'EmailFeedSetting'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"}) + }, + 'askbot.favoritequestion': { + 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"}) + }, + 'askbot.group': { + 'Meta': {'object_name': 'Group', '_ormbases': ['auth.Group']}, + 'description': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'described_group'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}), + 'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}), + 'is_vip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}), + 'moderate_answers_to_enquirers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'openness': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}), + 'preapproved_email_domains': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'preapproved_emails': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}) + }, + 'askbot.groupmembership': { + 'Meta': {'object_name': 'GroupMembership', '_ormbases': ['auth.AuthUserGroups']}, + 'authusergroups_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.AuthUserGroups']", 'unique': 'True', 'primary_key': 'True'}), + 'level': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}) + }, + 'askbot.markedtag': { + 'Meta': {'object_name': 'MarkedTag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"}) + }, + 'askbot.post': { + 'Meta': {'object_name': 'Post'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}), + 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_posts'", 'null': 'True', 'to': "orm['auth.User']"}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_posts'", 'symmetrical': 'False', 'through': "orm['askbot.PostToGroup']", 'to': "orm['askbot.Group']"}), + 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_posts'", 'null': 'True', 'to': "orm['auth.User']"}), + 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': "orm['auth.User']"}), + 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}), + 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'summary': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'posts'", 'null': 'True', 'blank': 'True', 'to': "orm['askbot.Thread']"}), + 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'askbot.postflagreason': { + 'Meta': {'object_name': 'PostFlagReason'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'details': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post_reject_reasons'", 'to': "orm['askbot.Post']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'askbot.postrevision': { + 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'}, + 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'approved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'approved_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}), + 'by_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'revised_at': ('django.db.models.fields.DateTimeField', [], {}), + 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '125', 'blank': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'}) + }, + 'askbot.posttogroup': { + 'Meta': {'unique_together': "(('post', 'group'),)", 'object_name': 'PostToGroup', 'db_table': "'askbot_post_groups'"}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']"}) + }, + 'askbot.questionview': { + 'Meta': {'object_name': 'QuestionView'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Post']"}), + 'when': ('django.db.models.fields.DateTimeField', [], {}), + 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"}) + }, + 'askbot.questionwidget': { + 'Meta': {'object_name': 'QuestionWidget'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order_by': ('django.db.models.fields.CharField', [], {'default': "'-added_at'", 'max_length': '18'}), + 'question_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '7'}), + 'search_query': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'style': ('django.db.models.fields.TextField', [], {'default': '"\\n@import url(\'http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700\');\\nbody {\\n overflow: hidden;\\n}\\n\\n#container {\\n width: 200px;\\n height: 350px;\\n}\\nul {\\n list-style: none;\\n padding: 5px;\\n margin: 5px;\\n}\\nli {\\n border-bottom: #CCC 1px solid;\\n padding-bottom: 5px;\\n padding-top: 5px;\\n}\\nli:last-child {\\n border: none;\\n}\\na {\\n text-decoration: none;\\n color: #464646;\\n font-family: \'Yanone Kaffeesatz\', sans-serif;\\n font-size: 15px;\\n}\\n"', 'blank': 'True'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'askbot.replyaddress': { + 'Meta': {'object_name': 'ReplyAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}), + 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reply_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'reply_action': ('django.db.models.fields.CharField', [], {'default': "'auto_answer_or_comment'", 'max_length': '32'}), + 'response_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'askbot.repute': { + 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"}, + 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}), + 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'askbot.tag': { + 'Meta': {'ordering': "('-used_count', 'name')", 'object_name': 'Tag', 'db_table': "u'tag'"}, + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'suggested_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'suggested_tags'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'tag_wiki': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'described_tag'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}), + 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'askbot.thread': { + 'Meta': {'object_name': 'Thread'}, + 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.FavoriteQuestion']", 'to': "orm['auth.User']"}), + 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_threads'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_threads'", 'symmetrical': 'False', 'through': "orm['askbot.ThreadToGroup']", 'to': "orm['askbot.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': "orm['auth.User']"}), + 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}), + 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'askbot.threadtogroup': { + 'Meta': {'unique_together': "(('thread', 'group'),)", 'object_name': 'ThreadToGroup', 'db_table': "'askbot_thread_groups'"}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}), + 'visibility': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}) + }, + 'askbot.vote': { + 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}), + 'vote': ('django.db.models.fields.SmallIntegerField', [], {}), + 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"}) + }, + 'auth.authusergroups': { + 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'AuthUserGroups', 'db_table': "'auth_user_groups'", 'managed': 'False'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['askbot'] diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index e89b2cda..3a46f119 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -13,6 +13,7 @@ import collections import datetime import hashlib import logging +import re import urllib import uuid from celery import states @@ -20,6 +21,7 @@ from celery.task import task from django.core.urlresolvers import reverse, NoReverseMatch from django.db.models import signals as django_signals from django.template import Context +from django.template.loader import get_template from django.utils.translation import ugettext as _ from django.utils.translation import ungettext from django.utils.safestring import mark_safe @@ -56,12 +58,19 @@ from askbot.models.repute import Award, Repute, Vote from askbot.models.widgets import AskWidget, QuestionWidget from askbot import auth from askbot.utils.decorators import auto_now_timestamp +from askbot.utils.markup import URL_RE from askbot.utils.slug import slugify +from askbot.utils.html import replace_links_with_text from askbot.utils.html import sanitize_html from askbot.utils.diff import textDiff as htmldiff from askbot.utils.url_utils import strip_path from askbot import mail +from django import get_version as django_version + +if django_version() > '1.3.1': + from askbot.models.message import Message + def get_model(model_name): """a shortcut for getting model for an askbot app""" return models.get_model('askbot', model_name) @@ -113,6 +122,38 @@ def get_users_by_text_query(search_query, users_query_set = None): # models.Q(about__search = search_query) # ) +class RelatedObjectSimulator(object): + '''Objects that simulates the "messages_set" related field + somehow django does not creates it automatically in django1.4.1''' + + def __init__(self, user, model_class): + self.user = user + self.model_class = model_class + + def create(self, **kwargs): + return self.model_class.objects.create(user=self.user, **kwargs) + + def filter(self, *args, **kwargs): + return self.model_class.objects.filter(*args, **kwargs) + + +#django 1.4.1 only +@property +def user_message_set(self): + return RelatedObjectSimulator(self, Message) + +#django 1.4.1 only +def user_get_and_delete_messages(self): + messages = [] + for message in Message.objects.filter(user=self): + messages.append(message) + message.delete() + return messages + +if django_version() > '1.3.1': + User.add_to_class('message_set', user_message_set) + User.add_to_class('get_and_delete_messages', user_get_and_delete_messages) + User.add_to_class( 'status', models.CharField( @@ -633,6 +674,19 @@ def user_assert_can_upload_file(request_user): ) +def user_assert_can_post_text(self, text): + """Raises exceptions.PermissionDenied, if user does not have + privilege to post given text, depending on the contents + """ + if re.search(URL_RE, text): + min_rep = askbot_settings.MIN_REP_TO_SUGGEST_LINK + if self.is_authenticated() and self.reputation < min_rep: + message = _( + 'Could not post, because your karma is insufficient to publish links' + ) + raise django_exceptions.PermissionDenied(message) + + def user_assert_can_post_question(self): """raises exceptions.PermissionDenied with text that has the reason for the denial @@ -2478,6 +2532,25 @@ def _process_vote(user, post, timestamp=None, cancel=False, vote_type=None): ) return vote +def user_fix_html_links(self, text): + """depending on the user's privilege, allow links + and hotlinked images or replace them with plain text + url + """ + is_simple_user = not self.is_administrator_or_moderator() + has_low_rep = self.reputation < askbot_settings.MIN_REP_TO_INSERT_LINK + if is_simple_user and has_low_rep: + result = replace_links_with_text(text) + if result != text: + message = ungettext( + 'At least %d karma point is required to post links', + 'At least %d karma points is required to post links', + askbot_settings.MIN_REP_TO_INSERT_LINK + ) % askbot_settings.MIN_REP_TO_INSERT_LINK + self.message_set.create(message=message) + return result + return text + def user_unfollow_question(self, question = None): self.followed_threads.remove(question.thread) @@ -2790,6 +2863,7 @@ User.add_to_class('get_tag_filtered_questions', user_get_tag_filtered_questions) User.add_to_class('get_messages', get_messages) User.add_to_class('delete_messages', delete_messages) User.add_to_class('toggle_favorite_question', toggle_favorite_question) +User.add_to_class('fix_html_links', user_fix_html_links) User.add_to_class('follow_question', user_follow_question) User.add_to_class('unfollow_question', user_unfollow_question) User.add_to_class('is_following_question', user_is_following_question) @@ -2846,6 +2920,7 @@ User.add_to_class('assert_can_upload_file', user_assert_can_upload_file) User.add_to_class('assert_can_post_question', user_assert_can_post_question) User.add_to_class('assert_can_post_answer', user_assert_can_post_answer) User.add_to_class('assert_can_post_comment', user_assert_can_post_comment) +User.add_to_class('assert_can_post_text', user_assert_can_post_text) User.add_to_class('assert_can_edit_post', user_assert_can_edit_post) User.add_to_class('assert_can_edit_deleted_post', user_assert_can_edit_deleted_post) User.add_to_class('assert_can_see_deleted_post', user_assert_can_see_deleted_post) @@ -3365,9 +3440,8 @@ def send_respondable_email_validation_message( ) data['email_code'] = reply_address.address - from askbot.skins.loaders import get_template template = get_template(template_name) - body_text = template.render(Context(data)) + body_text = template.render(Context(data))#todo: set lang reply_to_address = 'welcome-%s@%s' % ( reply_address.address, @@ -3509,7 +3583,6 @@ def moderate_group_joining(sender, instance=None, created=False, **kwargs): content_object = group ) - #signal for User model save changes django_signals.pre_save.connect(make_admin_if_first_user, sender=User) django_signals.pre_save.connect(calculate_gravatar_hash, sender=User) diff --git a/askbot/models/message.py b/askbot/models/message.py new file mode 100644 index 00000000..5086f515 --- /dev/null +++ b/askbot/models/message.py @@ -0,0 +1,25 @@ +'''Copied from Django 1.3.1 source code, it will use this model to''' +from django.db import models +from django.contrib.auth.models import User +from django.utils.translation import ugettext as _ + +class Message(models.Model): + """ + The message system is a lightweight way to queue messages for given + users. A message is associated with a User instance (so it is only + applicable for registered users). There's no concept of expiration or + timestamps. Messages are created by the Django admin after successful + actions. For example, "The poll Foo was created successfully." is a + message. + """ + user = models.ForeignKey(User, related_name='_message_set') + message = models.TextField(_('message')) + + class Meta: + '''Added for backwards compatibility with databases + migrated from django 1.3''' + app_label = 'auth' + db_table = 'auth_message' + + def __unicode__(self): + return self.message diff --git a/askbot/models/post.py b/askbot/models/post.py index daf9c93f..ce7b249b 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -503,8 +503,8 @@ class Post(models.Model): last_revision = self.html data = self.parse_post_text() + self.html = author.fix_html_links(data['html']) - self.html = data['html'] newly_mentioned_users = set(data['newly_mentioned_users']) - set([author]) removed_mentions = data['removed_mentions'] @@ -862,8 +862,8 @@ class Post(models.Model): if quote_level > 0, the post will be indented that number of times todo: move to views? """ - from askbot.skins.loaders import get_template from django.template import Context + from django.template.loader import get_template template = get_template('email/quoted_post.html') data = { 'post': self, @@ -871,7 +871,7 @@ class Post(models.Model): 'is_leaf_post': is_leaf_post, 'format': format } - return template.render(Context(data)) + return template.render(Context(data))#todo: set lang def format_for_email_as_parent_thread_summary(self): """format for email as summary of parent posts @@ -884,17 +884,6 @@ class Post(models.Model): if parent_post is None: break quote_level += 1 - """ - output += '<p>' - output += _( - 'In reply to %(user)s %(post)s of %(date)s' - ) % { - 'user': parent_post.author.username, - 'post': _(parent_post.post_type), - 'date': parent_post.added_at.strftime(const.DATETIME_FORMAT) - } - output += '</p>' - """ output += parent_post.format_for_email( quote_level = quote_level, format = 'parent_subthread' @@ -906,10 +895,10 @@ class Post(models.Model): """outputs question or answer and all it's comments returns empty string for all other post types """ - from askbot.skins.loaders import get_template from django.template import Context + from django.template.loader import get_template template = get_template('email/post_as_subthread.html') - return template.render(Context({'post': self})) + return template.render(Context({'post': self}))#todo: set lang def set_cached_comments(self, comments): """caches comments in the lifetime of the object @@ -2212,4 +2201,5 @@ class AnonymousAnswer(DraftContent): wiki=self.wiki, text=self.text ) + self.question.thread.invalidate_cached_data() self.delete() diff --git a/askbot/models/question.py b/askbot/models/question.py index 7faf589b..5c0d5732 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -8,9 +8,11 @@ from django.contrib.auth.models import User from django.core import cache # import cache, not from cache import cache, to be able to monkey-patch cache.cache in test cases from django.core import exceptions as django_exceptions from django.core.urlresolvers import reverse +from django.template.loader import get_template from django.utils.hashcompat import md5_constructor from django.utils.translation import ugettext as _ from django.utils.translation import ungettext +from django.utils.translation import string_concat import askbot from askbot.conf import settings as askbot_settings @@ -29,7 +31,6 @@ from askbot import const from askbot.utils.lists import LazyList from askbot.search import mysql from askbot.utils.slug import slugify -from askbot.skins.loaders import get_template #jinja2 template loading enviroment from askbot.search.state_manager import DummySearchState class ThreadQuerySet(models.query.QuerySet): @@ -702,7 +703,10 @@ class Thread(models.Model): def format_for_email(self, user=None): """experimental function: output entire thread for email""" - question, answers, junk, published_ans_ids = self.get_cached_post_data(user=user) + + question, answers, junk, published_ans_ids = \ + self.get_cached_post_data(user=user) + output = question.format_for_email_as_subthread() if answers: answer_heading = ungettext( @@ -1415,16 +1419,35 @@ class AnonymousQuestion(DraftContent): tagnames = models.CharField(max_length=125) is_anonymous = models.BooleanField(default=False) - def publish(self,user): + def publish(self, user): added_at = datetime.datetime.now() #todo: wrong - use User.post_question() instead - Thread.objects.create_new( - title = self.title, - added_at = added_at, - author = user, - wiki = self.wiki, - is_anonymous = self.is_anonymous, - tagnames = self.tagnames, - text = self.text, - ) - self.delete() + try: + user.assert_can_post_text(self.text) + Thread.objects.create_new( + title = self.title, + added_at = added_at, + author = user, + wiki = self.wiki, + is_anonymous = self.is_anonymous, + tagnames = self.tagnames, + text = self.text, + ) + self.delete() + except django_exceptions.PermissionDenied, error: + #delete previous draft questions (only one is allowed anyway) + prev_drafts = DraftQuestion.objects.filter(author=user) + prev_drafts.delete() + #convert this question to draft + DraftQuestion.objects.create( + author=user, + title=self.title, + text=self.text, + tagnames=self.tagnames + ) + #add message with a link to the ask page + extra_message = _( + 'Please, <a href="%s">review your question</a>.' + ) % reverse('ask') + message = string_concat(unicode(error), u' ', extra_message) + user.message_set.create(message=unicode(message)) diff --git a/askbot/models/repute.py b/askbot/models/repute.py index a6e9d7d1..e48773e6 100644 --- a/askbot/models/repute.py +++ b/askbot/models/repute.py @@ -223,7 +223,7 @@ class Repute(models.Model): return _('<em>Changed by moderator. Reason:</em> %(reason)s') \ % {'reason':self.comment} else: - delta = self.positive - self.negative + delta = self.positive + self.negative#.negative is < 0 so we add! link_title_data = { 'points': abs(delta), 'username': self.user.username, diff --git a/askbot/models/tag.py b/askbot/models/tag.py index d7e91eb5..e4c4540a 100644 --- a/askbot/models/tag.py +++ b/askbot/models/tag.py @@ -1,5 +1,4 @@ import re -import logging from django.db import models from django.contrib.auth.models import User from django.utils.translation import ugettext as _ diff --git a/askbot/patches/__init__.py b/askbot/patches/__init__.py index ebaeabac..6145097c 100644 --- a/askbot/patches/__init__.py +++ b/askbot/patches/__init__.py @@ -17,6 +17,9 @@ def patch_django(): django_patches.add_csrf_protection() django_patches.add_available_attrs_decorator() + if major == 1 and minor <=2: + django_patches.add_render_shortcut() + def patch_coffin(): """coffin before version 0.3.4 does not have csrf_token template tag. diff --git a/askbot/patches/django_patches.py b/askbot/patches/django_patches.py index e28c88ba..fe0e2fe7 100644 --- a/askbot/patches/django_patches.py +++ b/askbot/patches/django_patches.py @@ -339,3 +339,16 @@ def add_available_attrs_decorator(): return tuple(a for a in WRAPPER_ASSIGNMENTS if hasattr(fn, a)) import django.utils.decorators django.utils.decorators.available_attrs = available_attrs + +def add_render_shortcut(): + """adds `render` shortcut, introduced with django 1.3""" + try: + from django.shortcuts import render + except ImportError: + def render(request, template, data=None): + from django.shortcuts import render_to_response + from django.template import RequestContext + return render_to_response(template, RequestContext(request, data)) + + import django.shortcuts + django.shortcuts.render = render diff --git a/askbot/setup_templates/settings.py b/askbot/setup_templates/settings.py index b43b2b50..237a1280 100644 --- a/askbot/setup_templates/settings.py +++ b/askbot/setup_templates/settings.py @@ -87,8 +87,8 @@ SECRET_KEY = 'sdljdfjkldsflsdjkhsjkldgjlsdgfs s ' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', #below is askbot stuff for this tuple #'askbot.skins.loaders.load_template_source', #changed due to bug 97 'askbot.skins.loaders.filesystem_load_template_source', @@ -144,7 +144,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'askbot.context.application_settings', #'django.core.context_processors.i18n', 'askbot.user_messages.context_processors.user_messages',#must be before auth - 'django.core.context_processors.auth', #this is required for admin + 'django.contrib.auth.context_processors.auth', #this is required for admin 'django.core.context_processors.csrf', #necessary for csrf protection ) diff --git a/askbot/setup_templates/settings.py.mustache b/askbot/setup_templates/settings.py.mustache index aa153435..1f3dabd2 100644 --- a/askbot/setup_templates/settings.py.mustache +++ b/askbot/setup_templates/settings.py.mustache @@ -19,12 +19,16 @@ ADMINS = ( MANAGERS = ADMINS -DATABASE_ENGINE = 'postgresql_psycopg2' # only postgres (>8.3) and mysql are supported so far others have not been tested yet -DATABASE_NAME = '{{database_name}}' # Or path to database file if using sqlite3. -DATABASE_USER = '{{database_user}}' # Not used with sqlite3. -DATABASE_PASSWORD = '{{database_password}}' # Not used with sqlite3. -DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. -DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': '{{database_name}}', # Or path to database file if using sqlite3. + 'USER': '{{database_user}}', # Not used with sqlite3. + 'PASSWORD': '{{database_password}}', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} #outgoing mail server settings SERVER_EMAIL = '' @@ -87,10 +91,9 @@ SECRET_KEY = 'sdljdfjkldsflsdjkhsjkldgjlsdgfs s ' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', - #below is askbot stuff for this tuple - 'askbot.skins.loaders.filesystem_load_template_source', + 'askbot.skins.loaders.Loader', + 'django.template.loaders.app_directories.Loader', + 'django.template.loaders.filesystem.Loader', #'django.template.loaders.eggs.load_template_source', ) @@ -143,7 +146,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'askbot.context.application_settings', #'django.core.context_processors.i18n', 'askbot.user_messages.context_processors.user_messages',#must be before auth - 'django.core.context_processors.auth', #this is required for admin + '{{ auth_context_processor }}', #this is required for the admin app 'django.core.context_processors.csrf', #necessary for csrf protection ) diff --git a/askbot/skins/loaders.py b/askbot/skins/loaders.py index fc680f00..50276f34 100644 --- a/askbot/skins/loaders.py +++ b/askbot/skins/loaders.py @@ -1,6 +1,7 @@ import os.path -from django.template.loaders import filesystem +from django.template.loader import BaseLoader from django.template import RequestContext +from django.template import TemplateDoesNotExist from django.http import HttpResponse from django.utils import translation from django.conf import settings as django_settings @@ -22,27 +23,6 @@ template.add_to_builtins('askbot.templatetags.extra_filters_jinja') #here it is ignored because it is assumed that we won't use unicode paths ASKBOT_SKIN_COLLECTION_DIR = os.path.dirname(__file__) -#changed the name from load_template_source -def filesystem_load_template_source(name, dirs=None): - """Django template loader - """ - - if dirs is None: - dirs = (ASKBOT_SKIN_COLLECTION_DIR, ) - else: - dirs += (ASKBOT_SKIN_COLLECTION_DIR, ) - - try: - #todo: move this to top after splitting out get_skin_dirs() - tname = os.path.join(askbot_settings.ASKBOT_DEFAULT_SKIN, 'templates', name) - return filesystem.load_template_source(tname, dirs) - except: - tname = os.path.join('default', 'templates', name) - return filesystem.load_template_source(tname, dirs) -filesystem_load_template_source.is_usable = True -#added this for backward compatbility -load_template_source = filesystem_load_template_source - class SkinEnvironment(CoffinEnvironment): """Jinja template environment that loads templates from askbot skins @@ -103,8 +83,9 @@ def get_skin(request = None): for a given request (request var is not used at this time)""" return SKINS[askbot_settings.ASKBOT_DEFAULT_SKIN] -def get_template(template, request = None): - """retreives template for the skin +def get_askbot_template(template, request = None): + """ + retreives template for the skin request variable will be used in the future to set template according to the user preference or admins preference @@ -117,21 +98,22 @@ def get_template(template, request = None): def render_into_skin_as_string(template, data, request): context = RequestContext(request, data) - template = get_template(template, request) + template = get_askbot_template(template, request) return template.render(context) -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 - """ - return HttpResponse( - render_into_skin_as_string(template, data, request), - mimetype=mimetype - ) - def render_text_into_skin(text, data, request): context = RequestContext(request, data) skin = get_skin(request) template = skin.from_string(text) return template.render(context) + +class Loader(BaseLoader): + """skins template loader for Django > 1.2 + todo: verify that this actually follows django's convention correctly + """ + is_usable = True + def load_template(self, template_name, template_dirs = None): + try: + return get_askbot_template(template_name), template_name + except TemplateNotFound: + raise TemplateDoesNotExist diff --git a/askbot/startup_procedures.py b/askbot/startup_procedures.py index 6b28f688..1fa1b3e7 100644 --- a/askbot/startup_procedures.py +++ b/askbot/startup_procedures.py @@ -7,12 +7,13 @@ question: why not run these from askbot/__init__.py? the main function is run_startup_tests """ -import sys +import askbot +import django import os import re -import urllib -import askbot import south +import sys +import urllib from django.db import transaction, connection from django.conf import settings as django_settings from django.core.exceptions import ImproperlyConfigured @@ -44,7 +45,7 @@ class AskbotConfigError(ImproperlyConfigured): def askbot_warning(line): """prints a warning with the nice header, but does not quit""" - print >> sys.stderr, PREAMBLE + '\n' + line + print >> sys.stderr, line def print_errors(error_messages, header = None, footer = None): """if there is one or more error messages, @@ -210,14 +211,28 @@ def test_encoding(): def test_template_loader(): """Sends a warning if you have an old style template loader that used to send a warning""" - old_template_loader = 'askbot.skins.loaders.load_template_source' - if old_template_loader in django_settings.TEMPLATE_LOADERS: - raise AskbotConfigError( - "\nPlease change: \n" - "'askbot.skins.loaders.load_template_source', to\n" - "'askbot.skins.loaders.filesystem_load_template_source',\n" - "in the TEMPLATE_LOADERS of your settings.py file" + old_loaders = ( + 'askbot.skins.loaders.load_template_source', + 'askbot.skins.loaders.filesystem_load_template_source', + ) + errors = list() + for loader in old_loaders: + if loader in django_settings.TEMPLATE_LOADERS: + errors.append( + 'remove "%s" from the TEMPLATE_LOADERS setting' % loader + ) + + current_loader = 'askbot.skins.loaders.Loader' + if current_loader not in django_settings.TEMPLATE_LOADERS: + errors.append( + 'add "%s" to the beginning of the TEMPLATE_LOADERS' % current_loader ) + elif django_settings.TEMPLATE_LOADERS[0] != current_loader: + errors.append( + '"%s" must be the first element of TEMPLATE_LOADERS' % current_loader + ) + + print_errors(errors) def test_celery(): """Tests celery settings @@ -348,7 +363,6 @@ def test_new_skins(): def test_staticfiles(): """tests configuration of the staticfiles app""" errors = list() - import django django_version = django.VERSION if django_version[0] == 1 and django_version[1] < 3: staticfiles_app_name = 'staticfiles' @@ -458,7 +472,6 @@ def test_staticfiles(): ' python manage.py collectstatic\n' ) - print_errors(errors) if django_settings.STATICFILES_STORAGE == \ 'django.contrib.staticfiles.storage.StaticFilesStorage': @@ -686,6 +699,74 @@ def test_longerusername(): errors.append('run "python manage.py migrate longerusername"') print_errors(errors) +def test_template_context_processors(): + """makes sure that all necessary template context processors + are in the settings.py""" + + required_processors = [ + 'django.core.context_processors.request', + 'askbot.context.application_settings', + 'askbot.user_messages.context_processors.user_messages', + 'django.core.context_processors.csrf', + ] + old_auth_processor = 'django.core.context_processors.auth' + new_auth_processor = 'django.contrib.auth.context_processors.auth' + + invalid_processors = list() + if django.VERSION[1] <= 3: + required_processors.append(old_auth_processor) + if new_auth_processor in django_settings.TEMPLATE_CONTEXT_PROCESSORS: + invalid_processors.append(new_auth_processor) + elif django.VERSION[1] > 3: + required_processors.append(new_auth_processor) + if old_auth_processor in django_settings.TEMPLATE_CONTEXT_PROCESSORS: + invalid_processors.append(old_auth_processor) + + missing_processors = list() + for processor in required_processors: + if processor not in django_settings.TEMPLATE_CONTEXT_PROCESSORS: + missing_processors.append(processor) + + errors = list() + if invalid_processors: + message = 'remove from TEMPLATE_CONTEXT_PROCESSORS in settings.py:\n' + message += format_as_text_tuple_entries(invalid_processors) + errors.append(message) + + if missing_processors: + message = 'add to TEMPLATE_CONTEXT_PROCESSORS in settings.py:\n' + message += format_as_text_tuple_entries(missing_processors) + errors.append(message) + + print_errors(errors) + + +def test_cache_backend(): + """prints a warning if cache backend is disabled or per-process""" + if django.VERSION[1] > 2: + backend = django_settings.CACHES['default']['BACKEND'] + else: + backend = django_settings.CACHE_BACKEND + + errors = list() + if backend.strip() == '' or 'dummy' in backend: + message = """Please enable at least a "locmem" cache (for a single process server). +If you need to run > 1 server process, set up some production caching system, +such as redis or memcached""" + errors.append(message) + + if 'locmem' in backend: + message = """WARNING!!! You are using a 'locmem' (local memory) caching backend, +which is OK for a low volume site running on a single-process server. +For a multi-process configuration it is neccessary to have a production +cache system, such as redis or memcached. + +With local memory caching and multi-process setup you might intermittently +see outdated content on your site. +""" + askbot_warning(message) + + def test_group_messaging(): """tests correctness of the "group_messaging" app configuration""" errors = list() @@ -717,6 +798,7 @@ def test_group_messaging(): if errors: print_errors(errors) + def run_startup_tests(): """function that runs all startup tests, mainly checking settings config so far @@ -731,6 +813,7 @@ def run_startup_tests(): test_middleware() test_celery() #test_csrf_cookie_domain() + test_template_context_processors() test_tinymce() test_staticfiles() test_new_skins() @@ -738,6 +821,7 @@ def run_startup_tests(): test_avatar() test_group_messaging() test_haystack() + test_cache_backend() settings_tester = SettingsTester({ 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY': { 'value': True, diff --git a/askbot/tasks.py b/askbot/tasks.py index 505597d2..0ae96c09 100644 --- a/askbot/tasks.py +++ b/askbot/tasks.py @@ -24,6 +24,7 @@ import uuid from django.contrib.contenttypes.models import ContentType from django.template import Context +from django.template.loader import get_template from django.utils.translation import ugettext as _ from celery.decorators import task from askbot.conf import settings as askbot_settings @@ -80,7 +81,6 @@ def notify_author_of_published_revision_celery_task(revision): } #load the template - from askbot.skins.loaders import get_template template = get_template('email/notify_author_about_approved_post.html') #todo: possibly add headers to organize messages in threads headers = {'Reply-To': append_content_address} @@ -191,7 +191,6 @@ def send_instant_notifications_about_activity_in_post( return #calculate some variables used in the loop below - from askbot.skins.loaders import get_template update_type_map = const.RESPONSE_ACTIVITY_TYPE_MAP_FOR_TEMPLATES update_type = update_type_map[update_activity.activity_type] origin_post = post.get_origin_post() @@ -243,4 +242,3 @@ def send_instant_notifications_about_activity_in_post( ) else: logger.debug('success %s, logId=%s' % (user.email, log_id)) - diff --git a/askbot/templates/404.html b/askbot/templates/404.html index 158bfb94..2da99646 100644 --- a/askbot/templates/404.html +++ b/askbot/templates/404.html @@ -1,5 +1,44 @@ -{% load extra_tags %} -{% include_jinja "404.jinja.html" request %} -{% comment %} -this one has to be a django template because of use of default hander404 -{% endcomment %} +{% extends "one_column_body.html" %} +<!-- template 404.jinja.html --> +{% block title %}{% spaceless %}{% trans %}Page not found{% endtrans %}{% endspaceless %}{% endblock %} +{% block forestyle%} +<style type="text/css"> + form input { margin-right: 5px; } +</style> +{% endblock %} +{% block content %} +<h1>{% trans %}Page not found{% endtrans %}</h1> +<div id="main-body"> + <div style="padding:5px 0px 10px 0;line-height:25px;"> + <h2>{% trans %}Sorry, could not find the page you requested.{% endtrans %}</h2> + <div style="margin-top:5px"> + {% trans %}This might have happened for the following reasons:{% endtrans %}<br/> + <ul> + <li>{% trans %}this question or answer has been deleted;{% endtrans %}</li> + <li>{% trans %}url has error - please check it;{% endtrans %}</li> + <li>{% trans %}the page you tried to visit is protected or you don't have sufficient points, see{% endtrans %} <a href="{% url faq %}">{% trans %}faq{% endtrans %}</a>;</li> + <li>{% trans %}if you believe this error 404 should not have occured, please{% endtrans %} + <a href="{{feedback_site_url}}" target="_blank">{% trans %}report this problem{% endtrans %}</a></li> + </u> + </div> + <script type="text/javascript"> + var GOOG_FIXURL_LANG = '{{settings.LANGUAGE_CODE}}'; + var GOOG_FIXURL_SITE = '{{site_url}}'; + </script> + <script type="text/javascript" src="http://linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script> + <ul> + <li><a href="#" id="linkPrevious">{% trans %}back to previous page{% endtrans %} »</li> + <li><a href="{% url questions %}">{% trans %}see all questions{% endtrans %} »</a></li> + <li><a href="{% url tags %}">{% trans %}see all tags{% endtrans %} »</a></li> + </u> + </div> +</div> +{% endblock %} +{% block endjs %} + <script type="text/javascript"> + $().ready(function(){ + $("#linkPrevious").bind("click", back=function(){history.go(-1);}) + }); + </script> +{% endblock %} +<!-- end template 404.jinja.html --> diff --git a/askbot/templates/404.jinja.html b/askbot/templates/404.jinja.html deleted file mode 100644 index 2da99646..00000000 --- a/askbot/templates/404.jinja.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "one_column_body.html" %} -<!-- template 404.jinja.html --> -{% block title %}{% spaceless %}{% trans %}Page not found{% endtrans %}{% endspaceless %}{% endblock %} -{% block forestyle%} -<style type="text/css"> - form input { margin-right: 5px; } -</style> -{% endblock %} -{% block content %} -<h1>{% trans %}Page not found{% endtrans %}</h1> -<div id="main-body"> - <div style="padding:5px 0px 10px 0;line-height:25px;"> - <h2>{% trans %}Sorry, could not find the page you requested.{% endtrans %}</h2> - <div style="margin-top:5px"> - {% trans %}This might have happened for the following reasons:{% endtrans %}<br/> - <ul> - <li>{% trans %}this question or answer has been deleted;{% endtrans %}</li> - <li>{% trans %}url has error - please check it;{% endtrans %}</li> - <li>{% trans %}the page you tried to visit is protected or you don't have sufficient points, see{% endtrans %} <a href="{% url faq %}">{% trans %}faq{% endtrans %}</a>;</li> - <li>{% trans %}if you believe this error 404 should not have occured, please{% endtrans %} - <a href="{{feedback_site_url}}" target="_blank">{% trans %}report this problem{% endtrans %}</a></li> - </u> - </div> - <script type="text/javascript"> - var GOOG_FIXURL_LANG = '{{settings.LANGUAGE_CODE}}'; - var GOOG_FIXURL_SITE = '{{site_url}}'; - </script> - <script type="text/javascript" src="http://linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script> - <ul> - <li><a href="#" id="linkPrevious">{% trans %}back to previous page{% endtrans %} »</li> - <li><a href="{% url questions %}">{% trans %}see all questions{% endtrans %} »</a></li> - <li><a href="{% url tags %}">{% trans %}see all tags{% endtrans %} »</a></li> - </u> - </div> -</div> -{% endblock %} -{% block endjs %} - <script type="text/javascript"> - $().ready(function(){ - $("#linkPrevious").bind("click", back=function(){history.go(-1);}) - }); - </script> -{% endblock %} -<!-- end template 404.jinja.html --> diff --git a/askbot/templates/500.jinja.html b/askbot/templates/500.html index 297ae736..297ae736 100644 --- a/askbot/templates/500.jinja.html +++ b/askbot/templates/500.html diff --git a/askbot/templates/answer_edit.html b/askbot/templates/answer_edit.html index 8c3687f1..20c0684d 100644 --- a/askbot/templates/answer_edit.html +++ b/askbot/templates/answer_edit.html @@ -28,7 +28,10 @@ {% if settings.WIKI_ON and answer.wiki == False %} {{ macros.checkbox_in_div(form.wiki) }} {% endif %} - {% if request.user.is_authenticated() and request.user.can_make_group_private_posts() %} + {% if settings.GROUPS_ENABLED and + request.user.is_authenticated() and + request.user.can_make_group_private_posts() + %} {{ macros.checkbox_in_div(form.post_privately) }} {% endif %} <div class="after-editor"> diff --git a/askbot/templates/main_page/tab_bar.html b/askbot/templates/main_page/tab_bar.html index 17ab810e..37f63012 100644 --- a/askbot/templates/main_page/tab_bar.html +++ b/askbot/templates/main_page/tab_bar.html @@ -1,6 +1,7 @@ {% import "macros.html" as macros %} {% load extra_filters_jinja %} {% cache 0 "scope_sort_tabs" search_tags request.user author_name scope sort query context.page language_code %} +{% if settings.RSS_ENABLED %} <a class="rss" {% if feed_url %} href="{{feed_url}}" @@ -10,6 +11,7 @@ title="{% trans %}subscribe to the questions feed{% endtrans %}" >{% trans %}RSS{% endtrans %} </a> +{% endif %} <div class="tabBar"> <div id="sort_tabs" class="tabsA"> <span class="label">{% trans %}Sort by »{% endtrans %}</span> diff --git a/askbot/templates/question/new_answer_form.html b/askbot/templates/question/new_answer_form.html index 76772abf..bc51f44a 100644 --- a/askbot/templates/question/new_answer_form.html +++ b/askbot/templates/question/new_answer_form.html @@ -51,7 +51,10 @@ {% if settings.WIKI_ON %} {{ macros.checkbox_in_div(answer.wiki) }} {% endif %} - {% if request.user.is_authenticated() and request.user.can_make_group_private_posts() %} + {% if settings.GROUPS_ENABLED and + request.user.is_authenticated() and + request.user.can_make_group_private_posts() + %} {{ macros.checkbox_in_div(answer.post_privately) }} {% endif %} {% endif %} diff --git a/askbot/templates/question/sidebar.html b/askbot/templates/question/sidebar.html index 4d431ef2..905ce781 100644 --- a/askbot/templates/question/sidebar.html +++ b/askbot/templates/question/sidebar.html @@ -39,12 +39,14 @@ <input type="checkbox" id="question-subscribe-sidebar"/> <label for="question-subscribe-sidebar">{% trans %}<strong>Here</strong> (once you log in) you will be able to sign up for the periodic email updates about this question.{% endtrans %}</label> {%endif%} + {% if settings.RSS_ENABLED %} <p class="rss"> <a href="{{settings.APP_URL}}/feeds/question/{{ question.id }}" title="{% trans %}subscribe to this question rss feed{% endtrans %}" >{% trans %}subscribe to rss feed{% endtrans %}</a> </p> + {% endif %} </div> </div> diff --git a/askbot/templates/question_edit.html b/askbot/templates/question_edit.html index f176b11d..8b049e55 100644 --- a/askbot/templates/question_edit.html +++ b/askbot/templates/question_edit.html @@ -38,7 +38,10 @@ {% if form.can_stay_anonymous() %} {{ macros.checkbox_in_div(form.reveal_identity) }} {% endif %} - {% if request.user.is_authenticated() and request.user.can_make_group_private_posts() %} + {% if settings.GROUPS_ENABLED and + request.user.is_authenticated() and + request.user.can_make_group_private_posts() + %} {{ macros.checkbox_in_div(form.post_privately) }} {% endif %} </div> diff --git a/askbot/templates/widgets/ask_form.html b/askbot/templates/widgets/ask_form.html index d528609f..0f851fee 100644 --- a/askbot/templates/widgets/ask_form.html +++ b/askbot/templates/widgets/ask_form.html @@ -39,7 +39,10 @@ {% if settings.ALLOW_ASK_ANONYMOUSLY %} {{ macros.checkbox_in_div(form.ask_anonymously) }} {% endif %} - {% if request.user.is_authenticated() and request.user.can_make_group_private_posts() %} + {% if settings.GROUPS_ENABLED and + request.user.is_authenticated() and + request.user.can_make_group_private_posts() + %} {{ macros.checkbox_in_div(form.post_privately) }} {% endif %} </div> diff --git a/askbot/templatetags/extra_tags.py b/askbot/templatetags/extra_tags.py index dc9da5fc..6b148b4b 100644 --- a/askbot/templatetags/extra_tags.py +++ b/askbot/templatetags/extra_tags.py @@ -1,11 +1,12 @@ import math from django import template +from django.template import RequestContext +from django.template.loader import get_template from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse from askbot.utils import functions from askbot.utils.slug import slugify -from askbot.skins.loaders import get_template from askbot.conf import settings as askbot_settings register = template.Library() @@ -90,8 +91,8 @@ class IncludeJinja(template.Node): self.request_var = template.Variable(request_var) def render(self, context): request = self.request_var.resolve(context) - jinja_template = get_template(self.filename, request) - return jinja_template.render(context) + jinja_template = get_template(self.filename) + return jinja_template.render(RequestContext(request, context)) @register.tag def include_jinja(parser, token): @@ -112,4 +113,3 @@ def include_jinja(parser, token): raise template.TemplateSyntaxError('file name must be quoted') return IncludeJinja(filename, request_var) - diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py index f4ae052b..bdb361e5 100644 --- a/askbot/tests/email_alert_tests.py +++ b/askbot/tests/email_alert_tests.py @@ -1037,7 +1037,11 @@ class PostApprovalTests(utils.AskbotTestCase): class AbsolutizeUrlsInEmailsTests(utils.AskbotTestCase): - @with_settings(MIN_REP_TO_TRIGGER_EMAIL=1, APP_URL='http://example.com/') + @with_settings( + MIN_REP_TO_TRIGGER_EMAIL=1, + APP_URL='http://example.com/', + MIN_REP_TO_INSERT_LINK=1 + ) def test_urls_are_absolute(self): u1 = self.create_user('u1') max_email = models.EmailFeedSetting.MAX_EMAIL_SCHEDULE diff --git a/askbot/tests/email_parsing_tests.py b/askbot/tests/email_parsing_tests.py index 905bff0a..3ed0908a 100644 --- a/askbot/tests/email_parsing_tests.py +++ b/askbot/tests/email_parsing_tests.py @@ -1,11 +1,12 @@ +# -*- coding: utf-8 -*- from django.conf import settings as django_settings -from askbot.skins.loaders import get_template from django.template import Context +from django.template.loader import get_template from askbot import mail from askbot import models from askbot.tests import utils -class EmailParseTests(utils.AskbotTestCase): +class EmailParsingTests(utils.AskbotTestCase): def setUp(self): self.template_name = 'email/welcome_lamson_on.html' @@ -22,4 +23,38 @@ class EmailParseTests(utils.AskbotTestCase): print '==================================================' print cleaned_body print "CLEANED BODY" - self.assertEquals(cleaned_body, self.expected_output) + self.assertEqual(cleaned_body, self.expected_output) + + def test_gmail_rich_text_response_stripped(self): + text = u'\n\nthis is my reply!\n\nOn Wed, Oct 31, 2012 at 1:45 AM, <kp@kp-dev.askbot.com> wrote:\n\n> **\n> ' + self.assertEqual(mail.extract_reply(text), 'this is my reply!') + + def test_gmail_plain_text_response_stripped(self): + text = u'\n\nthis is my another reply!\n\nOn Wed, Oct 31, 2012 at 1:45 AM, <kp@kp-dev.askbot.com> wrote:\n>\n> ' + self.assertEqual(mail.extract_reply(text), 'this is my another reply!') + + def test_yahoo_mail_response_stripped(self): + text = u'\n\nthis is my reply!\n\n\n\n________________________________\n From: "kp@kp-dev.askbot.com" <kp@kp-dev.askbot.com>\nTo: fadeev@rocketmail.com \nSent: Wednesday, October 31, 2012 2:41 AM\nSubject: "This is my test question"\n \n\n \n \n \n' + self.assertEqual(mail.extract_reply(text), 'this is my reply!') + + def test_kmail_plain_text_response_stripped(self): + text = u'On Monday 01 October 2012 21:22:44 you wrote: \n\nthis is my reply!' + self.assertEqual(mail.extract_reply(text), 'this is my reply!') + + def test_outlook_com_with_rtf_response_stripped(self): + text = u'outlook.com (new hotmail) with RTF on \n\nSubject: "Posting a question by email." \nFrom: kp@kp-dev.askbot.com \nTo: aj_fitoria@hotmail.com \nDate: Thu, 1 Nov 2012 16:30:27 +0000' + self.assertEqual( + mail.extract_reply(text), + 'outlook.com (new hotmail) with RTF on' + ) + self.assertEqual( + mail.extract_reply(text), + 'outlook.com (new hotmail) with RTF on' + ) + + def test_outlook_com_plain_text_response_stripped(self): + text = u'reply from hotmail without RTF \n________________________________ \n> Subject: "test with recovered signature" \n> From: kp@kp-dev.askbot.com \n> To: aj_fitoria@hotmail.com \n> Date: Thu, 1 Nov 2012 16:44:35 +0000' + self.assertEqual( + mail.extract_reply(text), + u'reply from hotmail without RTF' + ) diff --git a/askbot/tests/form_tests.py b/askbot/tests/form_tests.py index 90f4f4f2..fed38f87 100644 --- a/askbot/tests/form_tests.py +++ b/askbot/tests/form_tests.py @@ -1,5 +1,7 @@ from django import forms as django_forms +from django.contrib.auth.models import AnonymousUser from askbot.tests.utils import AskbotTestCase +from askbot.tests.utils import with_settings from askbot.conf import settings as askbot_settings from askbot import forms from askbot.utils import forms as util_forms @@ -53,9 +55,10 @@ class AskByEmailFormTests(AskbotTestCase): and makes sure that tags and title are parsed out""" setting_backup = askbot_settings.TAGS_ARE_REQUIRED askbot_settings.update('TAGS_ARE_REQUIRED', True) + user = AnonymousUser() for test_case in SUBJECT_LINE_CASES: self.data['subject'] = test_case[0] - form = forms.AskByEmailForm(self.data) + form = forms.AskByEmailForm(self.data, user=user) output = test_case[1] if output is None: self.assertFalse(form.is_valid()) @@ -75,10 +78,11 @@ class AskByEmailFormTests(AskbotTestCase): """loops through variants of the from field in the emails and tests the email address extractor""" + user = AnonymousUser() for test_case in EMAIL_CASES: self.data['sender'] = test_case[0] expected_result = test_case[1] - form = forms.AskByEmailForm(self.data) + form = forms.AskByEmailForm(self.data, user=user) if expected_result is None: self.assertFalse(form.is_valid()) else: @@ -196,9 +200,9 @@ class EditQuestionAnonymouslyFormTests(AskbotTestCase): data['reveal_identity'] = 'on' self.form = forms.EditQuestionForm( data, - question = question, - user = editor, - revision = question.get_latest_revision(), + question=question, + user=editor, + revision=question.get_latest_revision(), ) def test_reveal_identity_field(self): @@ -230,7 +234,7 @@ class AskFormTests(AskbotTestCase): } if ask_anonymously == True: data['ask_anonymously'] = 'on' - self.form = forms.AskForm(data) + self.form = forms.AskForm(data, user=AnonymousUser()) self.form.full_clean() def assert_anon_is(self, value): @@ -317,7 +321,7 @@ class AnswerEditorFieldTests(AskbotTestCase): def setUp(self): self.old_min_length = askbot_settings.MIN_ANSWER_BODY_LENGTH askbot_settings.update('MIN_ANSWER_BODY_LENGTH', 10) - self.field = forms.AnswerEditorField() + self.field = forms.AnswerEditorField(user=AnonymousUser()) def tearDown(self): askbot_settings.update('MIN_ANSWER_BODY_LENGTH', self.old_min_length) @@ -364,8 +368,6 @@ class PostAsSomeoneFormTests(AskbotTestCase): class AskWidgetFormTests(AskbotTestCase): - form = forms.AskWidgetForm - def setUp(self): self.good_data = {'title': "What's the price of a house in london?"} self.good_data_anon = {'title': "What's the price of a house in london?", @@ -374,12 +376,40 @@ class AskWidgetFormTests(AskbotTestCase): self.bad_data = {'title': ''} def test_valid_input(self): - form_object = self.form(include_text=False, data=self.good_data) - print form_object.errors + form_object = forms.AskWidgetForm( + include_text=False, data=self.good_data, user=AnonymousUser() + ) self.assertTrue(form_object.is_valid()) - form_object = self.form(include_text=False, data=self.good_data_anon) + form_object = forms.AskWidgetForm( + include_text=False, data=self.good_data_anon, user=AnonymousUser() + ) self.assertTrue(form_object.is_valid()) def test_invalid_input(self): - form_object = self.form(False, data=self.bad_data) + form_object = forms.AskWidgetForm( + include_text=False, data=self.bad_data, user=AnonymousUser() + ) self.assertFalse(form_object.is_valid()) + + +TEXT_WITH_LINK = 'blah blah http://example.com/url/image.png' +class EditorFieldTests(AskbotTestCase): + + def setUp(self): + self.user = self.create_user('user') + self.user.reputation = 5 + self.user.save() + + @with_settings(EDITOR_TYPE='markdown', MIN_REP_TO_SUGGEST_LINK=10) + def test_low_rep_user_cannot_post_links_markdown(self): + field = forms.EditorField(user=self.user) + self.assertRaises( + django_forms.ValidationError, field.clean, TEXT_WITH_LINK + ) + + @with_settings(EDITOR_TYPE='tinymce', MIN_REP_TO_SUGGEST_LINK=10) + def test_low_rep_user_cannot_post_links_tinymce(self): + field = forms.EditorField(user=self.user) + self.assertRaises( + django_forms.ValidationError, field.clean, TEXT_WITH_LINK + ) diff --git a/askbot/tests/page_load_tests.py b/askbot/tests/page_load_tests.py index 4efac0f0..6c820fef 100644 --- a/askbot/tests/page_load_tests.py +++ b/askbot/tests/page_load_tests.py @@ -194,19 +194,17 @@ class PageLoadTestCase(AskbotTestCase): 'get_groups_list', status_code=status_code ) + #self.try_url( + # 'individual_question_feed', + # kwargs={'pk':'one-tag'}, + # status_code=status_code) self.try_url( - 'feeds', - status_code=status_code, - kwargs={'url':'rss'}) + 'latest_questions_feed', + status_code=status_code) self.try_url( - 'feeds', - kwargs={'url':'rss'}, + 'latest_questions_feed', data={'tags':'one-tag'}, status_code=status_code) - #self.try_url( - # 'feeds', - # kwargs={'url':'question'}, - # status_code=status_code) self.try_url( 'about', status_code=status_code, diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py index e61fcd2d..031909fb 100644 --- a/askbot/tests/post_model_tests.py +++ b/askbot/tests/post_model_tests.py @@ -3,13 +3,13 @@ import datetime from operator import attrgetter import time from askbot.search.state_manager import SearchState -from askbot.skins.loaders import get_template from django.contrib.auth.models import User from django.core import cache, urlresolvers from django.core.cache.backends.dummy import DummyCache from django.core.cache.backends.locmem import LocMemCache from django.core.exceptions import ValidationError +from django.template.loader import get_template from askbot.tests.utils import AskbotTestCase from askbot.models import Post from askbot.models import PostRevision diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py index 30cb48be..5353586c 100644 --- a/askbot/tests/reply_by_email_tests.py +++ b/askbot/tests/reply_by_email_tests.py @@ -84,8 +84,8 @@ class ReplyAddressModelTests(AskbotTestCase): 'instruction': 'reply above this line' } msg = MockMessage( - "This is a test reply \n\nOn such and such someone" - "wrote something \n\n%s\nlorem ipsum " % (reply_separator), + "This is a test reply \n\nOn such and such someone " + "wrote: \n\n%s\nlorem ipsum " % (reply_separator), "user1@domain.com" ) msg['Subject'] = 'test subject' diff --git a/askbot/tests/utils_tests.py b/askbot/tests/utils_tests.py index c8526ad1..ed845f48 100644 --- a/askbot/tests/utils_tests.py +++ b/askbot/tests/utils_tests.py @@ -2,6 +2,7 @@ from django.test import TestCase from askbot.tests.utils import with_settings from askbot.utils.url_utils import urls_equal from askbot.utils.html import absolutize_urls +from askbot.utils.html import replace_links_with_text from askbot.conf import settings as askbot_settings class UrlUtilsTests(TestCase): @@ -17,11 +18,58 @@ class UrlUtilsTests(TestCase): self.assertTrue(e('http://cnn.com/path', 'http://cnn.com/path/', True)) self.assertFalse(e('http://cnn.com/path', 'http://cnn.com/path/')) - + + +class ReplaceLinksWithTextTests(TestCase): + """testing correctness of `askbot.utils.html.replace_links_with_text""" + + def test_local_link_not_replaced(self): + text = '<a href="/some-link">some link</a>' + self.assertEqual(replace_links_with_text(text), text) + + def test_link_without_url_replaced(self): + text = '<a>some link</a>' + self.assertEqual(replace_links_with_text(text), 'some link') + + def test_external_link_without_text_replaced(self): + text = '<a href="https://example.com/"></a>' + #in this case we delete the link + self.assertEqual(replace_links_with_text(text), '') + + def test_external_link_with_text_replaced(self): + text = '<a href="https://example.com/">some link</a>' + self.assertEqual( + replace_links_with_text(text), + 'https://example.com/ (some link)' + ) + + def test_local_image_not_replaced(self): + text = '<img src="/some-image.gif"/>' + self.assertEqual(replace_links_with_text(text), text) + + def test_local_url_with_hotlinked_image_replaced(self): + text = '<a href="/some-link"><img src="http://example.com/img.png" alt="picture""> some text</a>' + self.assertEqual( + replace_links_with_text(text), + '<a href="/some-link">http://example.com/img.png (picture) some text</a>' + ) + + def test_hotlinked_image_without_alt_replaced(self): + text = '<img src="https://example.com/some-image.gif"/>' + self.assertEqual( + replace_links_with_text(text), + 'https://example.com/some-image.gif' + ) + + def test_hotlinked_image_with_alt_replaced(self): + text = '<img src="https://example.com/some-image.gif" alt="picture"/>' + self.assertEqual( + replace_links_with_text(text), + 'https://example.com/some-image.gif (picture)' + ) class HTMLUtilsTests(TestCase): """tests for :mod:`askbot.utils.html` module""" - @with_settings(APP_URL='http://example.com') def test_absolutize_urls(self): text = """<img class="junk" src="/some.gif"> <img class="junk" src="/cat.gif"> <IMG SRC='/some.png'>""" diff --git a/askbot/urls.py b/askbot/urls.py index 362a16ee..4694b38c 100644 --- a/askbot/urls.py +++ b/askbot/urls.py @@ -2,6 +2,7 @@ askbot askbot url configuraion file """ import os.path +import django from django.conf import settings from django.conf.urls.defaults import url, patterns, include from django.conf.urls.defaults import handler500, handler404 @@ -218,6 +219,11 @@ urlpatterns = patterns('', views.meta.list_suggested_tags, name = 'list_suggested_tags' ), + + #feeds + url(r'^feeds/rss/$', RssLastestQuestionsFeed(), name="latest_questions_feed"), + url(r'^feeds/question/(?P<pk>\d+)/$', RssIndividualQuestionFeed(), name="individual_question_feed"), + url(#ajax only r'^%s$' % 'moderate-suggested-tag', views.commands.moderate_suggested_tag, @@ -473,16 +479,9 @@ urlpatterns = patterns('', views.widgets.question_widget, name = 'question_widget' ), - url( - r'^feeds/(?P<url>.*)/$', - 'django.contrib.syndication.views.feed', - {'feed_dict': feeds}, - name='feeds' - ), #upload url is ajax only url( r'^%s$' % _('upload/'), views.writers.upload, name='upload'), url(r'^%s$' % _('feedback/'), views.meta.feedback, name='feedback'), - #url(r'^feeds/rss/$', RssLastestQuestionsFeed, name="latest_questions_feed"), url( r'^doc/(?P<path>.*)$', 'django.views.static.serve', diff --git a/askbot/utils/console.py b/askbot/utils/console.py index a691d961..0c27cd23 100644 --- a/askbot/utils/console.py +++ b/askbot/utils/console.py @@ -91,7 +91,8 @@ class ProgressBar(object): self.iterable = iterable self.length = length self.counter = float(0) - self.barlen = 60 + self.max_barlen = 60 + self.curr_barlen = 0 self.progress = '' if message and length > 0: print message @@ -103,17 +104,33 @@ class ProgressBar(object): def print_progress_bar(self): """prints the progress bar""" - sys.stdout.write('\b'*len(self.progress)) + self.backspace_progress_percent() + + tics_to_write = 0 + if self.length < self.max_barlen: + tics_to_write = self.max_barlen/self.length + elif int(self.counter) % (self.length/self.max_barlen) == 0: + tics_to_write = 1 + + if self.curr_barlen + tics_to_write <= self.max_barlen: + sys.stdout.write('-' * tics_to_write) + self.curr_barlen += tics_to_write - if self.length < self.barlen: - sys.stdout.write('-'*(self.barlen/self.length)) - elif int(self.counter) % (self.length / self.barlen) == 0: - sys.stdout.write('-') + self.print_progress_percent() + + def backspace_progress_percent(self): + sys.stdout.write('\b'*len(self.progress)) + def print_progress_percent(self): + """prints percent of achieved progress""" self.progress = ' %.2f%%' % (100 * (self.counter/self.length)) sys.stdout.write(self.progress) sys.stdout.flush() + def finish_progress_bar(self): + """brint the last bars, to make all bars equal length""" + self.backspace_progress_percent() + sys.stdout.write('-' * (self.max_barlen - self.curr_barlen)) def next(self): @@ -121,7 +138,8 @@ class ProgressBar(object): result = self.iterable.next() except StopIteration: if self.length > 0: - self.print_progress_bar() + self.finish_progress_bar() + self.print_progress_percent() sys.stdout.write('\n') raise diff --git a/askbot/utils/html.py b/askbot/utils/html.py index 49eddee2..1d76fdb7 100644 --- a/askbot/utils/html.py +++ b/askbot/utils/html.py @@ -1,11 +1,11 @@ """Utilities for working with HTML.""" +from bs4 import BeautifulSoup import html5lib from html5lib import sanitizer, serializer, tokenizer, treebuilders, treewalkers import re import htmlentitydefs from urlparse import urlparse from django.core.urlresolvers import reverse -from django.utils.html import escape from askbot.conf import settings as askbot_settings class HTMLSanitizerMixin(sanitizer.HTMLSanitizerMixin): @@ -55,12 +55,47 @@ def absolutize_urls(html): img_replacement = '\g<prefix>"%s/\g<url>" style="max-width:500px;"' % askbot_settings.APP_URL replacement = '\g<prefix>"%s\g<url>"' % askbot_settings.APP_URL html = url_re1.sub(img_replacement, html) - html= url_re2.sub(img_replacement, html) + html = url_re2.sub(img_replacement, html) html = url_re3.sub(replacement, html) #temporal fix for bad regex with wysiwyg editor return url_re4.sub(replacement, html).replace('%s//' % askbot_settings.APP_URL, '%s/' % askbot_settings.APP_URL) +def replace_links_with_text(html): + """any absolute links will be replaced with the + url in plain text, same with any img tags + """ + def format_url_replacement(url, text): + url = url.strip() + text = text.strip() + url_domain = urlparse(url).netloc + if url and text and url_domain != text and url != text: + return '%s (%s)' % (url, text) + return url or text or '' + + soup = BeautifulSoup(html) + abs_url_re = r'^http(s)?://' + + images = soup.find_all('img') + for image in images: + url = image.get('src', '') + text = image.get('alt', '') + if url == '' or re.match(abs_url_re, url): + image.replaceWith(format_url_replacement(url, text)) + + links = soup.find_all('a') + for link in links: + url = link.get('href', '') + text = ''.join(link.text) or '' + + if text == '':#this is due to an issue with url inlining in comments + link.replaceWith('') + elif url == '' or re.match(abs_url_re, url): + link.replaceWith(format_url_replacement(url, text)) + + return unicode(soup.find('body').renderContents(), 'utf-8') + + def sanitize_html(html): """Sanitizes an HTML fragment.""" p = html5lib.HTMLParser(tokenizer=HTMLSanitizer, diff --git a/askbot/views/avatar_views.py b/askbot/views/avatar_views.py index 42d0b38a..94252860 100644 --- a/askbot/views/avatar_views.py +++ b/askbot/views/avatar_views.py @@ -1,10 +1,15 @@ -"""this is an unfortunate copy-paste (mostly) +""" +todo: remove this module - not needed any more + +this is an unfortunate copy-paste (mostly) from the avatar app - the reason is that django-avatar app does not support jinja templates """ import urllib from django.http import HttpResponseRedirect from django.template import RequestContext +from django.template.loader import get_template +from django.shortcuts import render from django.utils.translation import ugettext as _ from django.views.decorators import csrf from django.conf import settings @@ -17,7 +22,6 @@ from avatar.settings import AVATAR_MAX_AVATARS_PER_USER, AVATAR_DEFAULT_SIZE from avatar.util import get_primary_avatar, get_default_avatar_url from avatar.views import render_primary as django_avatar_render_primary -from askbot.skins.loaders import render_into_skin from askbot import models notification = False @@ -111,7 +115,7 @@ def add(request, extra_context=None, next_override=None, if extra_context: data.update(extra_context) - return render_into_skin('avatar/add.html', data, request) + return render(request, 'avatar/add.html', data) @login_required @csrf.csrf_protect @@ -153,7 +157,7 @@ def change(request, extra_context=None, next_override=None, if extra_context: data.update(extra_context) - return render_into_skin('avatar/change.html', data, request) + return render(request, 'avatar/change.html', data) @login_required @csrf.csrf_protect @@ -190,7 +194,7 @@ def delete(request, extra_context=None, next_override=None, *args, **kwargs): if extra_context: data.update(extra_context) - return render_into_skin('avatar/confirm_delete.html', data, request) + return render(request, 'avatar/confirm_delete.html', data) def render_primary(request, user_id = None, *args, **kwargs): user = models.User.objects.get(id = user_id) diff --git a/askbot/views/commands.py b/askbot/views/commands.py index 698bf2db..de5bb12b 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -20,6 +20,8 @@ from django.http import HttpResponseRedirect from django.http import HttpResponseForbidden from django.forms import ValidationError, IntegerField, CharField from django.shortcuts import get_object_or_404 +from django.shortcuts import render +from django.template.loader import get_template from django.views.decorators import csrf from django.utils import simplejson from django.utils.html import escape @@ -35,8 +37,7 @@ from askbot.utils import decorators from askbot.utils import url_utils from askbot.utils.forms import get_db_object_or_404 from askbot import mail -from django.template import Context -from askbot.skins.loaders import render_into_skin, get_template +from django.template import RequestContext from askbot.skins.loaders import render_into_skin_as_string from askbot.skins.loaders import render_text_into_skin from askbot import const @@ -115,7 +116,7 @@ def manage_inbox(request): 'post': post.html, 'reject_reason': reject_reason.details.html } - body_text = template.render(Context(data)) + body_text = template.render(RequestContext(request, data)) mail.send_mail( subject_line = _('your post was not accepted'), body_text = unicode(body_text), @@ -684,7 +685,7 @@ def subscribe_for_tags(request): return HttpResponseRedirect(reverse('index')) else: data = {'tags': tag_names} - return render_into_skin('subscribe_for_tags.html', data, request) + return render(request, 'subscribe_for_tags.html', data) else: all_tag_names = pure_tag_names + wildcards message = _('Please sign in to subscribe for: %(tags)s') \ @@ -769,7 +770,7 @@ def close(request, id):#close question 'question': question, 'form': form, } - return render_into_skin('close.html', data, request) + return render(request, 'close.html', data) except exceptions.PermissionDenied, e: request.user.message_set.create(message = unicode(e)) return HttpResponseRedirect(question.get_absolute_url()) @@ -798,7 +799,7 @@ def reopen(request, id):#re-open question 'closed_by_profile_url': closed_by_profile_url, 'closed_by_username': closed_by_username, } - return render_into_skin('reopen.html', data, request) + return render(request, 'reopen.html', data) except exceptions.PermissionDenied, e: request.user.message_set.create(message = unicode(e)) @@ -1348,7 +1349,7 @@ def get_editor(request): if 'config' not in request.GET: return HttpResponseForbidden() config = simplejson.loads(request.GET['config']) - form = forms.EditorForm(editor_attrs=config) + form = forms.EditorForm(editor_attrs=config, user=request.user) editor_html = render_text_into_skin( '{{ form.media }} {{ form.editor }}', {'form': form}, diff --git a/askbot/views/meta.py b/askbot/views/meta.py index 7b271219..bb152d5b 100644 --- a/askbot/views/meta.py +++ b/askbot/views/meta.py @@ -6,7 +6,9 @@ This module contains a collection of views displaying all sorts of secondary and from django.shortcuts import render_to_response, get_object_or_404 from django.core.urlresolvers import reverse from django.core.paginator import Paginator, EmptyPage, InvalidPage +from django.shortcuts import render from django.template import RequestContext, Template +from django.template.loader import get_template from django.http import HttpResponseRedirect, HttpResponse, Http404 from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ @@ -21,16 +23,16 @@ from askbot.utils.forms import get_next_url from askbot.mail import mail_moderators from askbot.models import BadgeData, Award, User, Tag from askbot.models import badges as badge_data -from askbot.skins.loaders import get_template, render_into_skin, render_text_into_skin +from askbot.skins.loaders import render_text_into_skin from askbot.utils.decorators import admins_only from askbot.utils.forms import get_next_url from askbot.utils import functions def generic_view(request, template = None, page_class = None): - """this may be not necessary, since it is just a rewrite of render_into_skin""" + """this may be not necessary, since it is just a rewrite of render""" if request is None: # a plug for strange import errors in django startup return render_to_response('django_error.html') - return render_into_skin(template, {'page_class': page_class}, request) + return render(request, template, {'page_class': page_class}) def config_variable(request, variable_name = None, mimetype = None): """Print value from the configuration settings @@ -47,7 +49,7 @@ def about(request, template='about.html'): 'page_class': 'meta', 'content': askbot_settings.FORUM_ABOUT } - return render_into_skin('static_page.html', data, request) + return render(request, 'static_page.html', data) def page_not_found(request, template='404.html'): return generic_view(request, template) @@ -60,7 +62,7 @@ def help(request): 'app_name': askbot_settings.APP_SHORT_NAME, 'page_class': 'meta' } - return render_into_skin('help.html', data, request) + return render(render, 'help.html', data) def faq(request): if askbot_settings.FORUM_FAQ.strip() != '': @@ -69,18 +71,14 @@ def faq(request): 'content': askbot_settings.FORUM_FAQ, 'page_class': 'meta', } - return render_into_skin( - 'static_page.html', - data, - request - ) + return render(request, 'static_page.html', data) else: data = { 'gravatar_faq_url': reverse('faq') + '#gravatar', 'ask_question_url': reverse('ask'), 'page_class': 'meta', } - return render_into_skin('faq_static.html', data, request) + return render(request, 'faq_static.html', data) @csrf.csrf_protect def feedback(request): @@ -100,13 +98,26 @@ def feedback(request): data=request.POST ) if form.is_valid(): + if not request.user.is_authenticated(): - data['email'] = form.cleaned_data.get('email',None) + data['email'] = form.cleaned_data.get('email', None) + else: + data['email'] = request.user.email + data['message'] = form.cleaned_data['message'] - data['name'] = form.cleaned_data.get('name',None) - template = get_template('email/feedback_email.txt', request) + data['name'] = form.cleaned_data.get('name', None) + template = get_template('email/feedback_email.txt') message = template.render(RequestContext(request, data)) - mail_moderators(_('Q&A forum feedback'), message) + + headers = {} + if data['email']: + headers = {'Reply-To': data['email']} + + mail_moderators( + _('Q&A forum feedback'), + message, + headers=headers + ) msg = _('Thanks for the feedback!') request.user.message_set.create(message=msg) return HttpResponseRedirect(get_next_url(request)) @@ -115,7 +126,7 @@ def feedback(request): initial={'next':get_next_url(request)}) data['form'] = form - return render_into_skin('feedback.html', data, request) + return render(request, 'feedback.html', data) feedback.CANCEL_MESSAGE=_('We look forward to hearing your feedback! Please, give it next time :)') def privacy(request): @@ -124,7 +135,7 @@ def privacy(request): 'page_class': 'meta', 'content': askbot_settings.FORUM_PRIVACY } - return render_into_skin('static_page.html', data, request) + return render(request, 'static_page.html', data) def badges(request):#user status/reputation system #todo: supplement database data with the stuff from badges.py @@ -148,7 +159,7 @@ def badges(request):#user status/reputation system 'mybadges' : my_badges, 'feedback_faq_url' : reverse('feedback'), } - return render_into_skin('badges.html', data, request) + return render(request, 'badges.html', data) def badge(request, id): #todo: supplement database data with the stuff from badges.py @@ -169,7 +180,7 @@ def badge(request, id): 'badge' : badge, 'page_class': 'meta', } - return render_into_skin('badge.html', data, request) + return render(request, 'badge.html', data) @admins_only def list_suggested_tags(request): @@ -209,4 +220,4 @@ def list_suggested_tags(request): 'page_title': _('Suggested tags'), 'paginator_context' : paginator_context, } - return render_into_skin('list_suggested_tags.html', data, request) + return render(request, 'list_suggested_tags.html', data) diff --git a/askbot/views/readers.py b/askbot/views/readers.py index 5a3e378b..f1b31b32 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -11,9 +11,11 @@ import logging import urllib import operator from django.shortcuts import get_object_or_404 +from django.shortcuts import render from django.http import HttpResponseRedirect, HttpResponse, Http404, HttpResponseNotAllowed from django.core.paginator import Paginator, EmptyPage, InvalidPage -from django.template import Context +from django.template.loader import get_template +from django.template import RequestContext from django.utils import simplejson from django.utils.html import escape from django.utils.translation import ugettext as _ @@ -41,7 +43,6 @@ from askbot.utils.decorators import anonymous_forbidden, ajax_only, get_only from askbot.search.state_manager import SearchState, DummySearchState from askbot.templatetags import extra_tags from askbot.conf import settings as askbot_settings -from askbot.skins.loaders import render_into_skin, get_template #jinja2 template loading enviroment from askbot.views import context # used in index page @@ -151,23 +152,31 @@ def questions(request, **kwargs): question_counter = question_counter % {'q_num': humanize.intcomma(q_count),} if q_count > page_size: - paginator_tpl = get_template('main_page/paginator.html', request) - paginator_html = paginator_tpl.render(Context({ - 'context': functions.setup_paginator(paginator_context), - 'questions_count': q_count, - 'page_size' : page_size, - 'search_state': search_state, - })) + paginator_tpl = get_template('main_page/paginator.html') + paginator_html = paginator_tpl.render( + RequestContext( + request, { + 'context': functions.setup_paginator(paginator_context), + 'questions_count': q_count, + 'page_size' : page_size, + 'search_state': search_state, + } + ) + ) else: paginator_html = '' - questions_tpl = get_template('main_page/questions_loop.html', request) - questions_html = questions_tpl.render(Context({ - 'threads': page, - 'search_state': search_state, - 'reset_method_count': reset_method_count, - 'request': request - })) + questions_tpl = get_template('main_page/questions_loop.html') + questions_html = questions_tpl.render( + RequestContext( + request, { + 'threads': page, + 'search_state': search_state, + 'reset_method_count': reset_method_count, + 'request': request + } + ) + ) ajax_data = { 'query_data': { @@ -226,7 +235,7 @@ def questions(request, **kwargs): 'feed_url': context_feed_url, } - return render_into_skin('main_page.html', template_data, request) + return render(request, 'main_page.html', template_data) def tags(request):#view showing a listing of available tags - plain list @@ -316,10 +325,9 @@ def tags(request):#view showing a listing of available tags - plain list 'search_state': SearchState(*[None for x in range(7)]) } - return render_into_skin('tags.html', data, request) + return render(request, 'tags.html', data) @csrf.csrf_protect -#@cache_page(60 * 5) def question(request, id):#refactor - long subroutine. display question body, answers and comments """view that displays body of the question and all answers to it @@ -549,7 +557,7 @@ def question(request, id):#refactor - long subroutine. display question body, an if drafts.count() > 0: initial['text'] = drafts[0].text - answer_form = AnswerForm(initial) + answer_form = AnswerForm(initial, user=request.user) user_can_post_comment = ( request.user.is_authenticated() and request.user.can_post_comment() @@ -599,7 +607,7 @@ def question(request, id):#refactor - long subroutine. display question body, an data.update(context.get_for_tag_editor()) - return render_into_skin('question.html', data, request) + return render(request, 'question.html', data) def revisions(request, id, post_type = None): assert post_type in ('question', 'answer') @@ -622,7 +630,7 @@ def revisions(request, id, post_type = None): 'post': post, 'revisions': revisions, } - return render_into_skin('revisions.html', data, request) + return render(request, 'revisions.html', data) @csrf.csrf_exempt @ajax_only diff --git a/askbot/views/users.py b/askbot/views/users.py index 16857d3f..414ac8b0 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -21,6 +21,7 @@ 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.shortcuts import render from django.http import HttpResponse, HttpResponseForbidden from django.http import HttpResponseRedirect, Http404 from django.utils.translation import ugettext as _ @@ -40,7 +41,6 @@ from askbot import models from askbot import exceptions from askbot.models.badges import award_badges_signal from askbot.models.tag import format_personal_group_name -from askbot.skins.loaders import render_into_skin from askbot.search.state_manager import SearchState from askbot.utils import url_utils from askbot.utils.loading import load_module @@ -200,7 +200,7 @@ def show_users(request, by_group=False, group_id=None, group_slug=None): 'group_openness_choices': group_openness_choices } - return render_into_skin('users.html', data, request) + return render(request, 'users.html', data) @csrf.csrf_protect def user_moderate(request, subject, context): @@ -294,7 +294,7 @@ def user_moderate(request, subject, context): 'user_status_changed': user_status_changed } context.update(data) - return render_into_skin('user_profile/user_moderate.html', context, request) + return render(request, 'user_profile/user_moderate.html', context) #non-view function def set_new_email(user, new_email, nomessage=False): @@ -356,7 +356,7 @@ def edit_user(request, id): 'support_custom_avatars': ('avatar' in django_settings.INSTALLED_APPS), 'view_user': user, } - return render_into_skin('user_profile/user_edit.html', data, request) + return render(request, 'user_profile/user_edit.html', data) def user_stats(request, user, context): question_filter = {} @@ -528,7 +528,7 @@ def user_stats(request, user, context): } context.update(data) - return render_into_skin('user_profile/user_stats.html', context, request) + return render(request, 'user_profile/user_stats.html', context) def user_recent(request, user, context): @@ -682,7 +682,7 @@ def user_recent(request, user, context): 'activities' : activities[:const.USER_VIEW_DATA_SIZE] } context.update(data) - return render_into_skin('user_profile/user_recent.html', context, request) + return render(request, 'user_profile/user_recent.html', context) #not a view - no direct url route here, called by `user_responses` @csrf.csrf_protect @@ -714,7 +714,7 @@ def show_group_join_requests(request, user, context): 'join_requests': join_requests } context.update(data) - return render_into_skin('user_inbox/group_join_requests.html', context, request) + return render(request, 'user_inbox/group_join_requests.html', context) @owner_or_moderator_required @@ -783,7 +783,7 @@ def user_responses(request, user, context): context['group_messaging_template_name'] = 'group_messaging/home.html' #here we take shortcut, because we don't care about #all the extra context loaded below - return render_into_skin('user_inbox/messages.html', context, request) + return render(request, 'user_inbox/messages.html', context) else: raise Http404 @@ -851,7 +851,7 @@ def user_responses(request, user, context): 'responses' : filtered_response_list, } context.update(data) - return render_into_skin('user_inbox/responses_and_flags.html', context, request) + return render(request, 'user_inbox/responses_and_flags.html', context) def user_network(request, user, context): if 'followit' not in django_settings.INSTALLED_APPS: @@ -862,7 +862,7 @@ def user_network(request, user, context): 'followers': user.get_followers(), } context.update(data) - return render_into_skin('user_profile/user_network.html', context, request) + return render(request, 'user_profile/user_network.html', context) @owner_or_moderator_required def user_votes(request, user, context): @@ -892,7 +892,7 @@ def user_votes(request, user, context): 'votes' : votes[:const.USER_VIEW_DATA_SIZE] } context.update(data) - return render_into_skin('user_profile/user_votes.html', context, request) + return render(request, 'user_profile/user_votes.html', context) def user_reputation(request, user, context): @@ -915,7 +915,7 @@ def user_reputation(request, user, context): 'reps': reps } context.update(data) - return render_into_skin('user_profile/user_reputation.html', context, request) + return render(request, 'user_profile/user_reputation.html', context) def user_favorites(request, user, context): @@ -933,7 +933,7 @@ def user_favorites(request, user, context): 'questions' : questions, } context.update(data) - return render_into_skin('user_profile/user_favorites.html', context, request) + return render(request, 'user_profile/user_favorites.html', context) @owner_or_moderator_required @@ -984,10 +984,10 @@ def user_email_subscriptions(request, user, context): 'action_status': action_status, } context.update(data) - return render_into_skin( + return render( + request, 'user_profile/user_email_subscriptions.html', - context, - request + context ) @csrf.csrf_protect @@ -1006,7 +1006,7 @@ def user_custom_tab(request, user, context): 'tab_name': tab_settings['SLUG'], 'page_title': page_title }) - return render_into_skin('user_profile/custom_tab.html', context, request) + return render(request, 'user_profile/custom_tab.html', context) USER_VIEW_CALL_TABLE = { 'stats': user_stats, @@ -1130,4 +1130,4 @@ def groups(request, id = None, slug = None): 'tab_name': scope, 'page_class': 'groups-page' } - return render_into_skin('groups.html', data, request) + return render(request, 'groups.html', data) diff --git a/askbot/views/widgets.py b/askbot/views/widgets.py index 8699cdf1..f607411d 100644 --- a/askbot/views/widgets.py +++ b/askbot/views/widgets.py @@ -1,16 +1,15 @@ from datetime import datetime -from django.core import exceptions -from django.template import Context +from django.template import RequestContext +from django.template.loader import get_template +from django.shortcuts import render from django.http import HttpResponse, Http404 from django.views.decorators import csrf from django.core.urlresolvers import reverse from django.shortcuts import redirect, get_object_or_404 -from django.views.decorators.cache import cache_page from django.contrib.auth.decorators import login_required -from askbot.skins.loaders import render_into_skin, get_template from askbot.conf import settings as askbot_settings from askbot.utils import decorators from askbot import models @@ -47,7 +46,7 @@ def widgets(request): 'question_widgets': models.QuestionWidget.objects.all().count(), 'page_class': 'widgets' } - return render_into_skin('embed/widgets.html', data, request) + return render(request, 'embed/widgets.html', data) @csrf.csrf_protect def ask_widget(request, widget_id): @@ -61,8 +60,11 @@ def ask_widget(request, widget_id): widget = get_object_or_404(models.AskWidget, id=widget_id) if request.method == "POST": - form = forms.AskWidgetForm(include_text=widget.include_text_field, - data=request.POST) + form = forms.AskWidgetForm( + include_text=widget.include_text_field, + data=request.POST, + user=request.user + ) if form.is_valid(): ask_anonymously = form.cleaned_data['ask_anonymously'] title = form.cleaned_data['title'] @@ -93,7 +95,7 @@ def ask_widget(request, widget_id): } if request.user.is_authenticated(): data_dict['author'] = request.user - question = post_question(data_dict, request) + #question = post_question(data_dict, request) return redirect('ask_by_widget_complete') else: request.session['widget_question'] = data_dict @@ -116,14 +118,17 @@ def ask_widget(request, widget_id): next_url = '%s?next=%s' % (reverse('widget_signin'), reverse('ask_by_widget')) return redirect(next_url) - form = forms.AskWidgetForm(include_text=widget.include_text_field) + form = forms.AskWidgetForm( + include_text=widget.include_text_field, + user=request.user + ) data = { 'form': form, 'widget': widget, 'editor_type': askbot_settings.EDITOR_TYPE } - return render_into_skin('embed/ask_by_widget.html', data, request) + return render(request, 'embed/ask_by_widget.html', data) @login_required def ask_widget_complete(request): @@ -138,7 +143,7 @@ def ask_widget_complete(request): del request.session['widget_css'] data = {'question_url': question_url, 'custom_css': custom_css} - return render_into_skin('embed/ask_widget_complete.html', data, request) + return render(request, 'embed/ask_widget_complete.html', data) @decorators.admins_only @@ -149,7 +154,7 @@ def list_widgets(request, model): 'widgets': widgets, 'widget_name': model } - return render_into_skin('embed/list_widgets.html', data, request) + return render(request, 'embed/list_widgets.html', data) @decorators.admins_only def create_widget(request, model): @@ -167,7 +172,7 @@ def create_widget(request, model): data = {'form': form, 'action': 'edit', 'widget_name': model} - return render_into_skin('embed/widget_form.html', data, request) + return render(request, 'embed/widget_form.html', data) @decorators.admins_only def edit_widget(request, model, widget_id): @@ -206,7 +211,7 @@ def edit_widget(request, model, widget_id): data = {'form': form, 'action': 'edit', 'widget_name': model} - return render_into_skin('embed/widget_form.html', data, request) + return render(request, 'embed/widget_form.html', data) @decorators.admins_only def delete_widget(request, model, widget_id): @@ -216,28 +221,35 @@ def delete_widget(request, model, widget_id): widget.delete() return redirect('list_widgets', model=model) else: - return render_into_skin('embed/delete_widget.html', - {'widget': widget, 'widget_name': model}, request) + return render( + request, + 'embed/delete_widget.html', + {'widget': widget, 'widget_name': model} + ) def render_ask_widget_js(request, widget_id): widget = get_object_or_404(models.AskWidget, pk=widget_id) variable_name = "AskbotAskWidget%d" % widget.id - content_tpl = get_template('embed/askbot_widget.js', request) - context_dict = {'widget': widget, - 'host': request.get_host(), - 'variable_name': variable_name} - content = content_tpl.render(Context(context_dict)) + content_tpl = get_template('embed/askbot_widget.js') + context_dict = { + 'widget': widget, + 'host': request.get_host(), + 'variable_name': variable_name + } + content = content_tpl.render(RequestContext(request, context_dict)) return HttpResponse(content, mimetype='text/javascript') def render_ask_widget_css(request, widget_id): widget = get_object_or_404(models.AskWidget, pk=widget_id) variable_name = "AskbotAskWidget%d" % widget.id - content_tpl = get_template('embed/askbot_widget.css', request) - context_dict = {'widget': widget, - 'host': request.get_host(), - 'editor_type': askbot_settings.EDITOR_TYPE, - 'variable_name': variable_name} - content = content_tpl.render(Context(context_dict)) + content_tpl = get_template('embed/askbot_widget.css') + context_dict = { + 'widget': widget, + 'host': request.get_host(), + 'editor_type': askbot_settings.EDITOR_TYPE, + 'variable_name': variable_name + } + content = content_tpl.render(RequestContext(request, context_dict)) return HttpResponse(content, mimetype='text/css') def question_widget(request, widget_id): @@ -271,4 +283,4 @@ def question_widget(request, widget_id): 'widget': widget } - return render_into_skin('embed/question_widget.html', data, request) + return render(request, 'embed/question_widget.html', data) diff --git a/askbot/views/writers.py b/askbot/views/writers.py index db7a24d2..da4ce6a1 100644 --- a/askbot/views/writers.py +++ b/askbot/views/writers.py @@ -14,6 +14,7 @@ import tempfile import time import urlparse from django.shortcuts import get_object_or_404 +from django.shortcuts import render from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404 @@ -29,7 +30,6 @@ from askbot import exceptions as askbot_exceptions from askbot import forms from askbot import models from askbot.conf import settings as askbot_settings -from askbot.skins.loaders import render_into_skin from askbot.utils import decorators from askbot.utils.forms import format_errors from askbot.utils.functions import diff_date @@ -192,7 +192,7 @@ def import_data(request): 'dump_upload_form': form, 'need_configuration': (not stackexchange.is_ready()) } - return render_into_skin('import_data.html', data, request) + return render(request, 'import_data.html', data) #@login_required #actually you can post anonymously, but then must register @csrf.csrf_protect @@ -211,7 +211,7 @@ def ask(request):#view used to ask a new question user can start posting a question anonymously but then must login/register in order for the question go be shown """ - form = forms.AskForm(request.REQUEST) + form = forms.AskForm(request.REQUEST, user=request.user) if request.method == 'POST': if form.is_valid(): timestamp = datetime.datetime.now() @@ -264,7 +264,7 @@ def ask(request):#view used to ask a new question return HttpResponseRedirect(url_utils.get_login_url()) if request.method == 'GET': - form = forms.AskForm() + form = forms.AskForm(user=request.user) draft_title = '' draft_text = '' @@ -302,7 +302,7 @@ def ask(request):#view used to ask a new question 'tag_names': list()#need to keep context in sync with edit_question for tag editor } data.update(context.get_for_tag_editor()) - return render_into_skin('ask.html', data, request) + return render(request, 'ask.html', data) @login_required @csrf.csrf_exempt @@ -349,7 +349,7 @@ def retag_question(request, id): 'question': question, 'form' : form, } - return render_into_skin('question_retag.html', data, request) + return render(request, 'question_retag.html', data) except exceptions.PermissionDenied, e: if request.is_ajax(): response_data = { @@ -386,24 +386,24 @@ def edit_question(request, id): rev_id = revision_form.cleaned_data['revision'] revision = question.revisions.get(revision = rev_id) form = forms.EditQuestionForm( - question = question, - user = request.user, - revision = revision + question=question, + user=request.user, + revision=revision ) else: form = forms.EditQuestionForm( request.POST, - question = question, - user = request.user, - revision = revision + question=question, + user=question.user, + revision=revision ) else:#new content edit # Always check modifications against the latest revision form = forms.EditQuestionForm( request.POST, - question = question, - revision = revision, - user = request.user, + question=question, + revision=revision, + user=request.user, ) revision_form = forms.RevisionForm(question, revision) if form.is_valid(): @@ -437,10 +437,10 @@ def edit_question(request, id): 'wiki': question.wiki } form = forms.EditQuestionForm( - question = question, - revision = revision, - user = request.user, - initial = initial + question=question, + revision=revision, + user=request.user, + initial=initial ) data = { @@ -455,7 +455,7 @@ def edit_question(request, id): 'category_tree_data': askbot_settings.CATEGORY_TREE } data.update(context.get_for_tag_editor()) - return render_into_skin('question_edit.html', data, request) + return render(request, 'question_edit.html', data) except exceptions.PermissionDenied, e: request.user.message_set.create(message = unicode(e)) @@ -481,15 +481,20 @@ def edit_answer(request, id): # Replace with those from the selected revision rev = revision_form.cleaned_data['revision'] revision = answer.revisions.get(revision = rev) - form = forms.EditAnswerForm(answer, revision) + form = forms.EditAnswerForm( + answer, revision, user=request.user + ) else: form = forms.EditAnswerForm( answer, revision, - request.POST + request.POST, + user=request.user ) else: - form = forms.EditAnswerForm(answer, revision, request.POST) + form = forms.EditAnswerForm( + answer, revision, request.POST, user=request.user + ) revision_form = forms.RevisionForm(answer, revision) if form.is_valid(): @@ -506,7 +511,7 @@ def edit_answer(request, id): return HttpResponseRedirect(answer.get_absolute_url()) else: revision_form = forms.RevisionForm(answer, revision) - form = forms.EditAnswerForm(answer, revision) + form = forms.EditAnswerForm(answer, revision, user=request.user) if request.user.can_make_group_private_posts(): form.initial['post_privately'] = answer.is_private() data = { @@ -517,7 +522,7 @@ def edit_answer(request, id): 'revision_form': revision_form, 'form': form, } - return render_into_skin('answer_edit.html', data, request) + return render(request, 'answer_edit.html', data) except exceptions.PermissionDenied, e: request.user.message_set.create(message = unicode(e)) @@ -536,7 +541,7 @@ def answer(request, id):#process a new answer """ question = get_object_or_404(models.Post, post_type='question', id=id) if request.method == "POST": - form = forms.AnswerForm(request.POST) + form = forms.AnswerForm(request.POST, user=request.user) if form.is_valid(): wiki = form.cleaned_data['wiki'] text = form.cleaned_data['text'] diff --git a/group_messaging/__init__.py b/group_messaging/__init__.py index 642ad5c8..ed3d73ff 100644 --- a/group_messaging/__init__.py +++ b/group_messaging/__init__.py @@ -11,4 +11,7 @@ the group should be named `'_personal_1'`. Only one person must be a member of a personal group and each user must have such group. + +TODO: decouple this application +first step is to package send_mail separately """ diff --git a/group_messaging/models.py b/group_messaging/models.py index 9cc12786..5d43baf8 100644 --- a/group_messaging/models.py +++ b/group_messaging/models.py @@ -4,7 +4,7 @@ import copy import datetime import urllib from askbot.mail import send_mail #todo: remove dependency? -from coffin.template.loader import get_template +from django.template.loader import get_template from django.db import models from django.db.models import signals from django.conf import settings as django_settings @@ -395,9 +395,10 @@ class Message(models.Model): #in the template we have a placeholder to be replaced like this: body_text = body_text.replace('THREAD_URL_HOLE', thread_url) send_mail( - recipient_list=[user.email,], - subject_line=subject, - body_text=body_text + subject, + body_text, + django_settings.DEFAULT_FROM_EMAIL, + [user.email,], ) diff --git a/group_messaging/views.py b/group_messaging/views.py index d4d646ec..244762d1 100644 --- a/group_messaging/views.py +++ b/group_messaging/views.py @@ -10,7 +10,7 @@ and turns them into complete views """ import copy import datetime -from coffin.template.loader import get_template +from django.template.loader import get_template from django.contrib.auth.models import User from django.db import models from django.forms import IntegerField |