summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--askbot/__init__.py1
-rw-r--r--askbot/api.py8
-rw-r--r--askbot/conf/__init__.py3
-rw-r--r--askbot/conf/group_settings.py20
-rw-r--r--askbot/conf/karma_and_badges_visibility.py50
-rw-r--r--askbot/conf/moderation.py22
-rw-r--r--askbot/conf/skin_general_settings.py2
-rw-r--r--askbot/const/__init__.py23
-rw-r--r--askbot/deps/livesettings/values.py6
-rw-r--r--askbot/doc/source/changelog.rst7
-rw-r--r--askbot/forms.py24
-rw-r--r--askbot/lamson_handlers.py83
-rw-r--r--askbot/management/commands/send_email_alerts.py3
-rw-r--r--askbot/migrations/0114_auto__add_groupprofile__add_groupmembership__add_field_tag_tag_wiki.py318
-rw-r--r--askbot/migrations/0115_auto__chg_field_post_thread.py296
-rw-r--r--askbot/migrations/0116_auto__add_field_groupprofile_logo_url__add_unique_emailfeedsetting_sub.py304
-rw-r--r--askbot/migrations/0117_auto__add_field_post_approved__add_field_thread_approved__add_field_po.py335
-rw-r--r--askbot/migrations/0118_auto__add_field_postrevision_by_email.py304
-rw-r--r--askbot/models/__init__.py273
-rw-r--r--askbot/models/post.py484
-rw-r--r--askbot/models/question.py55
-rw-r--r--askbot/models/reply_by_email.py42
-rw-r--r--askbot/models/tag.py54
-rw-r--r--askbot/models/user.py30
-rw-r--r--askbot/skins/common/media/js/post.js253
-rw-r--r--askbot/skins/common/media/js/user.js290
-rw-r--r--askbot/skins/common/media/js/utils.js218
-rw-r--r--askbot/skins/common/media/js/wmd/wmd.js3
-rw-r--r--askbot/skins/common/templates/authopenid/signin.html3
-rw-r--r--askbot/skins/common/templates/question/answer_author_info.html4
-rw-r--r--askbot/skins/common/templates/question/question_author_info.html4
-rw-r--r--askbot/skins/default/media/style/lib_style.less2
-rw-r--r--askbot/skins/default/media/style/style.less58
-rw-r--r--askbot/skins/default/templates/groups.html22
-rw-r--r--askbot/skins/default/templates/instant_notification.html39
-rw-r--r--askbot/skins/default/templates/instant_notification_reply_by_email.html14
-rw-r--r--askbot/skins/default/templates/macros.html94
-rw-r--r--askbot/skins/default/templates/main_page/headline.html4
-rw-r--r--askbot/skins/default/templates/main_page/sidebar.html4
-rw-r--r--askbot/skins/default/templates/meta/bottom_scripts.html3
-rw-r--r--askbot/skins/default/templates/question/sidebar.html4
-rw-r--r--askbot/skins/default/templates/reopen.html3
-rw-r--r--askbot/skins/default/templates/user_profile/user.html3
-rw-r--r--askbot/skins/default/templates/user_profile/user_inbox.html79
-rw-r--r--askbot/skins/default/templates/user_profile/user_info.html10
-rw-r--r--askbot/skins/default/templates/user_profile/user_network.html18
-rw-r--r--askbot/skins/default/templates/user_profile/user_recent.html3
-rw-r--r--askbot/skins/default/templates/user_profile/user_reputation.html3
-rw-r--r--askbot/skins/default/templates/user_profile/user_stats.html42
-rw-r--r--askbot/skins/default/templates/user_profile/user_tabs.html2
-rw-r--r--askbot/skins/default/templates/user_profile/user_votes.html3
-rw-r--r--askbot/skins/default/templates/users.html92
-rw-r--r--askbot/skins/default/templates/widgets/meta_nav.html10
-rw-r--r--askbot/skins/default/templates/widgets/question_summary.html5
-rw-r--r--askbot/skins/default/templates/widgets/user_list.html8
-rw-r--r--askbot/skins/default/templates/widgets/user_long_score_and_badge_summary.html36
-rw-r--r--askbot/skins/default/templates/widgets/user_navigation.html12
-rw-r--r--askbot/skins/default/templates/widgets/user_score_and_badge_summary.html32
-rw-r--r--askbot/skins/utils.py4
-rw-r--r--askbot/templatetags/extra_filters_jinja.py11
-rw-r--r--askbot/tests/form_tests.py1
-rw-r--r--askbot/tests/reply_by_email_tests.py50
-rw-r--r--askbot/urls.py42
-rw-r--r--askbot/utils/decorators.py2
-rw-r--r--askbot/utils/file_utils.py3
-rw-r--r--askbot/utils/mail.py134
-rw-r--r--askbot/views/commands.py150
-rw-r--r--askbot/views/meta.py2
-rw-r--r--askbot/views/readers.py9
-rw-r--r--askbot/views/users.py134
-rw-r--r--askbot/views/writers.py14
-rw-r--r--askbot_requirements.txt1
-rw-r--r--askbot_requirements_dev.txt1
73 files changed, 4118 insertions, 567 deletions
diff --git a/askbot/__init__.py b/askbot/__init__.py
index 5a8afcaa..f861033d 100644
--- a/askbot/__init__.py
+++ b/askbot/__init__.py
@@ -34,6 +34,7 @@ REQUIREMENTS = {
'openid': 'python-openid',
'pystache': 'pystache==0.3.1',
'lamson': 'Lamson',
+ 'pytz': 'pytz',
}
#necessary for interoperability of django and coffin
diff --git a/askbot/api.py b/askbot/api.py
index 8b788016..57d5c1aa 100644
--- a/askbot/api.py
+++ b/askbot/api.py
@@ -18,8 +18,14 @@ def get_info_on_moderation_items(user):
if not(user.is_moderator() or user.is_administrator()):
return None
+ content_types = (
+ const.TYPE_ACTIVITY_MARK_OFFENSIVE,
+ const.TYPE_ACTIVITY_MODERATED_NEW_POST,
+ const.TYPE_ACTIVITY_MODERATED_POST_EDIT,
+ )
+
messages = models.ActivityAuditStatus.objects.filter(
- activity__activity_type = const.TYPE_ACTIVITY_MARK_OFFENSIVE,
+ activity__activity_type__in = content_types,
user = user
)
diff --git a/askbot/conf/__init__.py b/askbot/conf/__init__.py
index dff91d8e..8378fca3 100644
--- a/askbot/conf/__init__.py
+++ b/askbot/conf/__init__.py
@@ -3,8 +3,10 @@ import askbot
import askbot.conf.minimum_reputation
import askbot.conf.vote_rules
import askbot.conf.reputation_changes
+import askbot.conf.karma_and_badges_visibility
import askbot.conf.email
import askbot.conf.forum_data_rules
+import askbot.conf.moderation
import askbot.conf.flatpages
import askbot.conf.site_settings
import askbot.conf.license
@@ -17,6 +19,7 @@ import askbot.conf.sidebar_profile
import askbot.conf.leading_sidebar
import askbot.conf.spam_and_moderation
import askbot.conf.user_settings
+import askbot.conf.group_settings
import askbot.conf.markup
import askbot.conf.social_sharing
import askbot.conf.badges
diff --git a/askbot/conf/group_settings.py b/askbot/conf/group_settings.py
new file mode 100644
index 00000000..a48fb55d
--- /dev/null
+++ b/askbot/conf/group_settings.py
@@ -0,0 +1,20 @@
+"""Group settings"""
+from askbot.conf.settings_wrapper import settings
+from askbot.conf.super_groups import LOGIN_USERS_COMMUNICATION
+from askbot.deps import livesettings
+from django.utils.translation import ugettext as _
+
+GROUP_SETTINGS = livesettings.ConfigurationGroup(
+ 'GROUP_SETTINGS',
+ _('Group settings'),
+ super_group = LOGIN_USERS_COMMUNICATION
+ )
+
+settings.register(
+ livesettings.BooleanValue(
+ GROUP_SETTINGS,
+ 'GROUPS_ENABLED',
+ default = False,
+ description = _('Enable user groups'),
+ )
+)
diff --git a/askbot/conf/karma_and_badges_visibility.py b/askbot/conf/karma_and_badges_visibility.py
new file mode 100644
index 00000000..4c75cb22
--- /dev/null
+++ b/askbot/conf/karma_and_badges_visibility.py
@@ -0,0 +1,50 @@
+"""
+Settings for making the karma and badge systems visible to
+the users at a different degree
+"""
+from django.utils.translation import ugettext as _
+from askbot.conf.settings_wrapper import settings
+from askbot.deps import livesettings
+from askbot.conf.super_groups import REP_AND_BADGES
+
+KARMA_AND_BADGE_VISIBILITY = livesettings.ConfigurationGroup(
+ 'KARMA_AND_BADGE_VISIBILITY',
+ _('Karma & Badge visibility'),
+ super_group = REP_AND_BADGES
+ )
+
+
+settings.register(
+ livesettings.StringValue(
+ KARMA_AND_BADGE_VISIBILITY,
+ 'KARMA_MODE',
+ default = 'public',
+ choices = (
+ ('public', 'show publicly'),
+ ('private', 'show to owners only'),
+ ('hidden', 'hide completely'),
+ ),#todo: later implement hidden mode
+ description = _("Visibility of karma"),
+ clear_cache = True,
+ help_text = _(
+ "User's karma may be shown publicly or only to the owners"
+ )
+ )
+)
+
+settings.register(
+ livesettings.StringValue(
+ KARMA_AND_BADGE_VISIBILITY,
+ 'BADGES_MODE',
+ default = 'public',
+ choices = (
+ ('public', 'show publicly'),
+ ('hidden', 'hide completely')
+ ),#todo: later implement private mode
+ description = _("Visibility of badges"),
+ clear_cache = True,
+ help_text = _(
+ 'Badges can be either publicly shown or completely hidden'
+ )
+ )
+)
diff --git a/askbot/conf/moderation.py b/askbot/conf/moderation.py
new file mode 100644
index 00000000..9f8e24c7
--- /dev/null
+++ b/askbot/conf/moderation.py
@@ -0,0 +1,22 @@
+"""Settings to control content moderation"""
+
+from askbot.conf.settings_wrapper import settings
+from askbot.conf.super_groups import DATA_AND_FORMATTING
+from askbot.deps.livesettings import ConfigurationGroup
+from askbot.deps.livesettings import BooleanValue
+from django.utils.translation import ugettext as _
+
+MODERATION = ConfigurationGroup(
+ 'MODERATION',
+ _('Content moderation'),
+ super_group = DATA_AND_FORMATTING
+ )
+
+settings.register(
+ BooleanValue(
+ MODERATION,
+ 'ENABLE_CONTENT_MODERATION',
+ default = False,
+ description = _('Enable content moderation'),
+ )
+)
diff --git a/askbot/conf/skin_general_settings.py b/askbot/conf/skin_general_settings.py
index ccecdaba..d81c984b 100644
--- a/askbot/conf/skin_general_settings.py
+++ b/askbot/conf/skin_general_settings.py
@@ -12,7 +12,7 @@ from askbot.conf.super_groups import CONTENT_AND_UI
GENERAL_SKIN_SETTINGS = ConfigurationGroup(
'GENERAL_SKIN_SETTINGS',
- _('Logos and HTML <head> parts'),
+ _('Skin, logos and HTML <head> parts'),
super_group = CONTENT_AND_UI
)
diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py
index 66e20dae..879dcb49 100644
--- a/askbot/const/__init__.py
+++ b/askbot/const/__init__.py
@@ -19,6 +19,7 @@ CLOSE_REASONS = (
)
LONG_TIME = 60*60*24*30 #30 days is a lot of time
+DATETIME_FORMAT = '%I:%M %p, %d %b %Y'
TYPE_REPUTATION = (
(1, 'gain_by_upvoted'),
@@ -51,6 +52,11 @@ POST_SORT_METHODS = (
('relevance-desc', _('relevance')),
)
+POST_TYPES = ('answer', 'comment', 'question', 'tag_wiki')
+
+REPLY_SEPARATOR_TEMPLATE = '==== %(user_action)s %(instruction)s -=-=='
+REPLY_SEPARATOR_REGEX = re.compile('==== .* -=-==', re.MULTILINE)
+
ANSWER_SORT_METHODS = (#no translations needed here
'latest', 'oldest', 'votes'
)
@@ -117,6 +123,10 @@ TYPE_ACTIVITY_EMAIL_UPDATE_SENT = 18
TYPE_ACTIVITY_MENTION = 19
TYPE_ACTIVITY_UNANSWERED_REMINDER_SENT = 20
TYPE_ACTIVITY_ACCEPT_ANSWER_REMINDER_SENT = 21
+TYPE_ACTIVITY_CREATE_TAG_WIKI = 22
+TYPE_ACTIVITY_UPDATE_TAG_WIKI = 23
+TYPE_ACTIVITY_MODERATED_NEW_POST = 24
+TYPE_ACTIVITY_MODERATED_POST_EDIT = 25
#TYPE_ACTIVITY_EDIT_QUESTION = 17
#TYPE_ACTIVITY_EDIT_ANSWER = 18
@@ -149,6 +159,19 @@ TYPE_ACTIVITY = (
_('reminder about accepting the best answer sent'),
),
(TYPE_ACTIVITY_MENTION, _('mentioned in the post')),
+ (
+ TYPE_ACTIVITY_CREATE_TAG_WIKI,
+ _('created tag description'),
+ ),
+ (
+ TYPE_ACTIVITY_UPDATE_TAG_WIKI,
+ _('updated tag description')
+ ),
+ (TYPE_ACTIVITY_MODERATED_NEW_POST, _('made a new post')),
+ (
+ TYPE_ACTIVITY_MODERATED_POST_EDIT,
+ _('made an edit')
+ ),
)
diff --git a/askbot/deps/livesettings/values.py b/askbot/deps/livesettings/values.py
index e5516d8b..95ca1069 100644
--- a/askbot/deps/livesettings/values.py
+++ b/askbot/deps/livesettings/values.py
@@ -6,6 +6,7 @@ from decimal import Decimal
from django import forms
from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
+from django.core.cache import cache
from django.utils import simplejson
from django.utils.datastructures import SortedDict
from django.utils.encoding import smart_str
@@ -149,6 +150,7 @@ class Value(object):
- `hidden` - If true, then render a hidden field.
- `default` - If given, then this Value will return that default whenever it has no assocated `Setting`.
- `update_callback` - if given, then this value will call the callback whenever updated
+ - `clear_cache` - if `True` - clear all the caches on updates
"""
self.group = group
self.key = key
@@ -159,6 +161,7 @@ class Value(object):
self.hidden = kwargs.pop('hidden', False)
self.update_callback = kwargs.pop('update_callback', None)
self.requires = kwargs.pop('requires', None)
+ self.clear_cache = kwargs.pop('clear_cache', False)
if self.requires:
reqval = kwargs.pop('requiresvalue', key)
if not is_list_or_tuple(reqval):
@@ -366,6 +369,9 @@ class Value(object):
s.save()
signals.configuration_value_changed.send(self, old_value=current_value, new_value=new_value, setting=self)
+
+ if self.clear_cache:
+ cache.clear()
return True
else:
diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst
index 1055caf7..eb1cd591 100644
--- a/askbot/doc/source/changelog.rst
+++ b/askbot/doc/source/changelog.rst
@@ -1,6 +1,13 @@
Changes in Askbot
=================
+Future version
+--------------
+* User groups (Evgeny)
+* Public/Private/Hidden reputation (Evgeny)
+* Enabling/disabling the badges system (Evgeny)
+* Created a basic post moderation feature (Evgeny)
+
0.7.40 (March 29, 2012)
-----------------------
* New data models!!! (`Tomasz ZieliƄski <http://pyconsultant.eu>`_)
diff --git a/askbot/forms.py b/askbot/forms.py
index 1816c202..f71a5520 100644
--- a/askbot/forms.py
+++ b/askbot/forms.py
@@ -876,6 +876,10 @@ class EditAnswerForm(forms.Form):
self.fields['text'].initial = revision.text
self.fields['wiki'].initial = answer.wiki
+class EditTagWikiForm(forms.Form):
+ text = forms.CharField()
+ tag_id = forms.IntegerField()
+
class EditUserForm(forms.Form):
email = forms.EmailField(
label=u'Email',
@@ -1135,3 +1139,23 @@ class SimpleEmailSubscribeForm(forms.Form):
else:
email_settings_form = EFF(initial=EFF.NO_EMAIL_INITIAL)
email_settings_form.save(user, save_unbound=True)
+
+class GroupLogoURLForm(forms.Form):
+ """form for saving group logo url"""
+ group_id = forms.IntegerField()
+ image_url = forms.CharField()
+
+class EditGroupMembershipForm(forms.Form):
+ """a form for adding or removing users
+ to and from user groups"""
+ user_id = forms.IntegerField()
+ group_name = forms.CharField()
+ action = forms.CharField()
+
+ def clean_action(self):
+ """allowed actions are 'add' and 'remove'"""
+ action = self.cleaned_data['action']
+ if action not in ('add', 'remove'):
+ del self.cleaned_data['action']
+ raise forms.ValidationError('invalid action')
+ return action
diff --git a/askbot/lamson_handlers.py b/askbot/lamson_handlers.py
index 488d8b12..f121af06 100644
--- a/askbot/lamson_handlers.py
+++ b/askbot/lamson_handlers.py
@@ -55,7 +55,12 @@ def is_attachment(part):
attachment"""
return get_disposition(part) == 'attachment'
-def process_attachment(part):
+def is_inline_attachment(part):
+ """True if part content disposition is
+ inline"""
+ return get_disposition(part) == 'inline'
+
+def format_attachment(part):
"""takes message part and turns it into SimpleUploadedFile object"""
att_info = get_attachment_info(part)
name = att_info.get('filename', None)
@@ -73,33 +78,48 @@ def is_body(part):
return True
return False
-def get_body(message):
- """returns plain text body of the message"""
- body = message.body()
- if body:
- return body
- for part in message.walk():
- if is_body(part):
- return part.body
+def get_part_type(part):
+ if is_body(part):
+ return 'body'
+ elif is_attachment(part):
+ return 'attachment'
+ elif is_inline_attachment(part):
+ return 'inline'
+
+def get_parts(message):
+ """returns list of tuples (<part_type>, <formatted_part>),
+ where <part-type> is one of 'body', 'attachment', 'inline'
+ and <formatted-part> - will be in the directly usable form:
+ * if it is 'body' - then it will be unicode text
+ * for attachment - it will be django's SimpleUploadedFile instance
+
+ There may be multiple 'body' parts as well as others
+ usually the body is split when there are inline attachments present.
+ """
+
+ parts = list()
+
+ if message.body():
+ parts.append(('body', message.body()))
-def get_attachments(message):
- """returns a list of file attachments
- represented by StringIO objects"""
- attachments = list()
for part in message.walk():
- if is_attachment(part):
- attachments.append(process_attachment(part))
- return attachments
+ part_type = get_part_type(part)
+ if part_type == 'body':
+ part_content = part.body
+ elif part_type in ('attachment', 'inline'):
+ part_content = format_attachment(part)
+ else:
+ continue
+ parts.append((part_type, part_content))
+ return parts
@route('ask@(host)')
@stateless
def ASK(message, host = None):
- body = get_body(message)
- attachments = get_attachments(message)
+ parts = get_parts(message)
from_address = message.From
subject = message['Subject']#why lamson does not give it normally?
- mail.process_emailed_question(from_address, subject, body, attachments)
-
+ mail.process_emailed_question(from_address, subject, parts)
@route('reply-(address)@(host)', address='.+')
@stateless
@@ -123,26 +143,11 @@ def PROCESS(message, address = None, host = None):
address = address,
allowed_from_email = message.From
)
- separator = _("======= Reply above this line. ====-=-=")
- parts = get_body(message).split(separator)
- attachments = get_attachments(message)
- if len(parts) != 2 :
- error = _("Your message was malformed. Please make sure to qoute \
- the original notification you received at the end of your reply.")
+ parts = get_parts(message)
+ if reply_address.was_used:
+ reply_address.edit_post(parts)
else:
- reply_part = parts[0]
- reply_part = '\n'.join(reply_part.splitlines(True)[:-3])
- #the function below actually posts to the forum
- if reply_address.was_used:
- reply_address.edit_post(
- reply_part.strip(),
- attachments = attachments
- )
- else:
- reply_address.create_reply(
- reply_part.strip(),
- attachments = attachments
- )
+ reply_address.create_reply(parts)
except ReplyAddress.DoesNotExist:
error = _("You were replying to an email address\
unknown to the system or you were replying from a different address from the one where you\
diff --git a/askbot/management/commands/send_email_alerts.py b/askbot/management/commands/send_email_alerts.py
index 8cb71859..218bd9a9 100644
--- a/askbot/management/commands/send_email_alerts.py
+++ b/askbot/management/commands/send_email_alerts.py
@@ -136,6 +136,9 @@ class Command(NoArgsCommand):
).exclude(
thread__closed=True
).order_by('-thread__last_activity_at')
+
+ if askbot_settings.ENABLE_CONTENT_MODERATION:
+ base_qs = base_qs.filter(approved = True)
#todo: for some reason filter on did not work as expected ~Q(viewed__who=user) |
# Q(viewed__who=user,viewed__when__lt=F('thread__last_activity_at'))
#returns way more questions than you might think it should
diff --git a/askbot/migrations/0114_auto__add_groupprofile__add_groupmembership__add_field_tag_tag_wiki.py b/askbot/migrations/0114_auto__add_groupprofile__add_groupmembership__add_field_tag_tag_wiki.py
new file mode 100644
index 00000000..35986359
--- /dev/null
+++ b/askbot/migrations/0114_auto__add_groupprofile__add_groupmembership__add_field_tag_tag_wiki.py
@@ -0,0 +1,318 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding model 'GroupProfile'
+ db.create_table('askbot_groupprofile', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('group_tag', self.gf('django.db.models.fields.related.OneToOneField')(related_name='group_profile', unique=True, to=orm['askbot.Tag'])),
+ ))
+ db.send_create_signal('askbot', ['GroupProfile'])
+
+ # Adding model 'GroupMembership'
+ db.create_table('askbot_groupmembership', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('group', self.gf('django.db.models.fields.related.ForeignKey')(related_name='user_memberships', to=orm['askbot.Tag'])),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='group_memberships', to=orm['auth.User'])),
+ ))
+ db.send_create_signal('askbot', ['GroupMembership'])
+
+ # Adding field 'Tag.tag_wiki'
+ db.add_column(u'tag', 'tag_wiki',
+ self.gf('django.db.models.fields.related.OneToOneField')(related_name='described_tag', unique=True, null=True, to=orm['askbot.Post']),
+ keep_default=False)
+
+ def backwards(self, orm):
+ # Deleting model 'GroupProfile'
+ db.delete_table('askbot_groupprofile')
+
+ # Deleting model 'GroupMembership'
+ db.delete_table('askbot_groupmembership')
+
+ # Deleting field 'Tag.tag_wiki'
+ db.delete_column(u'tag', 'tag_wiki_id')
+
+ 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.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.emailfeedsetting': {
+ 'Meta': {'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.groupmembership': {
+ 'Meta': {'object_name': 'GroupMembership'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_memberships'", 'to': "orm['askbot.Tag']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_memberships'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.groupprofile': {
+ 'Meta': {'object_name': 'GroupProfile'},
+ 'group_tag': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'group_profile'", 'unique': 'True', 'to': "orm['askbot.Tag']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ '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'}),
+ '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']"}),
+ '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']"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", '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.postrevision': {
+ 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}),
+ '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', [], {}),
+ 'revision_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ '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.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.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'", 'to': "orm['askbot.Post']"}),
+ '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'}),
+ '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'}),
+ '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']"}),
+ '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']"}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ '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.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.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_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_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'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ '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': '30'}),
+ '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'] \ No newline at end of file
diff --git a/askbot/migrations/0115_auto__chg_field_post_thread.py b/askbot/migrations/0115_auto__chg_field_post_thread.py
new file mode 100644
index 00000000..95c6e267
--- /dev/null
+++ b/askbot/migrations/0115_auto__chg_field_post_thread.py
@@ -0,0 +1,296 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Changing field 'Post.thread'
+ db.alter_column('askbot_post', 'thread_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['askbot.Thread']))
+ def backwards(self, orm):
+
+ # Changing field 'Post.thread'
+ db.alter_column('askbot_post', 'thread_id', self.gf('django.db.models.fields.related.ForeignKey')(default='zhopa', to=orm['askbot.Thread']))
+
+ 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.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.emailfeedsetting': {
+ 'Meta': {'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.groupmembership': {
+ 'Meta': {'object_name': 'GroupMembership'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_memberships'", 'to': "orm['askbot.Tag']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_memberships'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.groupprofile': {
+ 'Meta': {'object_name': 'GroupProfile'},
+ 'group_tag': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'group_profile'", 'unique': 'True', 'to': "orm['askbot.Tag']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ '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'}),
+ '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']"}),
+ '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']"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ '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.postrevision': {
+ 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}),
+ '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', [], {}),
+ 'revision_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ '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.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.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'", 'to': "orm['askbot.Post']"}),
+ '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'}),
+ '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'}),
+ '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']"}),
+ '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']"}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ '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.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.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_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_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'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ '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': '30'}),
+ '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/migrations/0116_auto__add_field_groupprofile_logo_url__add_unique_emailfeedsetting_sub.py b/askbot/migrations/0116_auto__add_field_groupprofile_logo_url__add_unique_emailfeedsetting_sub.py
new file mode 100644
index 00000000..12f123b3
--- /dev/null
+++ b/askbot/migrations/0116_auto__add_field_groupprofile_logo_url__add_unique_emailfeedsetting_sub.py
@@ -0,0 +1,304 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding field 'GroupProfile.logo_url'
+ db.add_column('askbot_groupprofile', 'logo_url',
+ self.gf('django.db.models.fields.URLField')(max_length=200, null=True),
+ keep_default=False)
+
+ # Adding unique constraint on 'EmailFeedSetting', fields ['subscriber', 'feed_type']
+ db.create_unique('askbot_emailfeedsetting', ['subscriber_id', 'feed_type'])
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'EmailFeedSetting', fields ['subscriber', 'feed_type']
+ db.delete_unique('askbot_emailfeedsetting', ['subscriber_id', 'feed_type'])
+
+ # Deleting field 'GroupProfile.logo_url'
+ db.delete_column('askbot_groupprofile', 'logo_url')
+
+ 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.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.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.groupmembership': {
+ 'Meta': {'object_name': 'GroupMembership'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_memberships'", 'to': "orm['askbot.Tag']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_memberships'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.groupprofile': {
+ 'Meta': {'object_name': 'GroupProfile'},
+ 'group_tag': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'group_profile'", 'unique': 'True', 'to': "orm['askbot.Tag']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'})
+ },
+ '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'}),
+ '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']"}),
+ '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']"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ '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.postrevision': {
+ 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}),
+ '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', [], {}),
+ 'revision_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ '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.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.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'", 'to': "orm['askbot.Post']"}),
+ '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'}),
+ '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'}),
+ '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']"}),
+ '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']"}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ '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.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.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_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_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'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ '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': '30'}),
+ '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'] \ No newline at end of file
diff --git a/askbot/migrations/0117_auto__add_field_post_approved__add_field_thread_approved__add_field_po.py b/askbot/migrations/0117_auto__add_field_post_approved__add_field_thread_approved__add_field_po.py
new file mode 100644
index 00000000..9dc9e1e5
--- /dev/null
+++ b/askbot/migrations/0117_auto__add_field_post_approved__add_field_thread_approved__add_field_po.py
@@ -0,0 +1,335 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding field 'Post.approved'
+ db.add_column('askbot_post', 'approved',
+ self.gf('django.db.models.fields.BooleanField')(default=True, db_index=True),
+ keep_default=False)
+
+ # Adding field 'Thread.approved'
+ db.add_column('askbot_thread', 'approved',
+ self.gf('django.db.models.fields.BooleanField')(default=True, db_index=True),
+ keep_default=False)
+
+ # Adding field 'PostRevision.approved'
+ db.add_column('askbot_postrevision', 'approved',
+ self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True),
+ keep_default=False)
+
+ # Adding field 'PostRevision.approved_by'
+ db.add_column('askbot_postrevision', 'approved_by',
+ self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True),
+ keep_default=False)
+
+ # Adding field 'PostRevision.approved_at'
+ db.add_column('askbot_postrevision', 'approved_at',
+ self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True),
+ keep_default=False)
+
+ def backwards(self, orm):
+ # Deleting field 'Post.approved'
+ db.delete_column('askbot_post', 'approved')
+
+ # Deleting field 'Thread.approved'
+ db.delete_column('askbot_thread', 'approved')
+
+ # Deleting field 'PostRevision.approved'
+ db.delete_column('askbot_postrevision', 'approved')
+
+ # Deleting field 'PostRevision.approved_by'
+ db.delete_column('askbot_postrevision', 'approved_by_id')
+
+ # Deleting field 'PostRevision.approved_at'
+ db.delete_column('askbot_postrevision', 'approved_at')
+
+ 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.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.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.groupmembership': {
+ 'Meta': {'object_name': 'GroupMembership'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_memberships'", 'to': "orm['askbot.Tag']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_memberships'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.groupprofile': {
+ 'Meta': {'object_name': 'GroupProfile'},
+ 'group_tag': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'group_profile'", 'unique': 'True', 'to': "orm['askbot.Tag']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'})
+ },
+ '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']"}),
+ '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']"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ '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.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']"}),
+ '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', [], {}),
+ 'revision_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ '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.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.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'", 'to': "orm['askbot.Post']"}),
+ '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'}),
+ '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']"}),
+ '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']"}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ '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.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.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_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_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'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ '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': '30'}),
+ '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'] \ No newline at end of file
diff --git a/askbot/migrations/0118_auto__add_field_postrevision_by_email.py b/askbot/migrations/0118_auto__add_field_postrevision_by_email.py
new file mode 100644
index 00000000..7c304d1e
--- /dev/null
+++ b/askbot/migrations/0118_auto__add_field_postrevision_by_email.py
@@ -0,0 +1,304 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding field 'PostRevision.by_email'
+ db.add_column('askbot_postrevision', 'by_email',
+ self.gf('django.db.models.fields.BooleanField')(default=False),
+ keep_default=False)
+
+ def backwards(self, orm):
+ # Deleting field 'PostRevision.by_email'
+ db.delete_column('askbot_postrevision', 'by_email')
+
+ 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.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.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.groupmembership': {
+ 'Meta': {'object_name': 'GroupMembership'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_memberships'", 'to': "orm['askbot.Tag']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_memberships'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.groupprofile': {
+ 'Meta': {'object_name': 'GroupProfile'},
+ 'group_tag': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'group_profile'", 'unique': 'True', 'to': "orm['askbot.Tag']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'})
+ },
+ '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']"}),
+ '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']"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ '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.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'}),
+ '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', [], {}),
+ 'revision_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ '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.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.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'", 'to': "orm['askbot.Post']"}),
+ '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'}),
+ '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']"}),
+ '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']"}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ '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.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.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_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_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'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ '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': '30'}),
+ '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'] \ No newline at end of file
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py
index 09ec0018..14989adb 100644
--- a/askbot/models/__init__.py
+++ b/askbot/models/__init__.py
@@ -30,6 +30,7 @@ from askbot.models.answer import AnonymousAnswer
from askbot.models.tag import Tag, MarkedTag
from askbot.models.meta import Vote
from askbot.models.user import EmailFeedSetting, ActivityAuditStatus, Activity
+from askbot.models.user import GroupMembership, GroupProfile
from askbot.models.post import Post, PostRevision
from askbot.models.reply_by_email import ReplyAddress
from askbot.models import signals
@@ -43,8 +44,16 @@ from askbot.utils.url_utils import strip_path
from askbot.utils import mail
def get_model(model_name):
+ """a shortcut for getting model for an askbot app"""
return models.get_model('askbot', model_name)
+def get_admins_and_moderators():
+ """returns query set of users who are site administrators
+ and moderators"""
+ return User.objects.filter(
+ models.Q(is_superuser=True) | models.Q(status='m')
+ )
+
User.add_to_class(
'status',
models.CharField(
@@ -254,6 +263,12 @@ def user_can_have_strong_url(self):
followed by the search engine crawlers"""
return (self.reputation >= askbot_settings.MIN_REP_TO_HAVE_STRONG_URL)
+def user_can_reply_by_email(self):
+ """True, if reply by email is enabled
+ and user has sufficient reputatiton"""
+ return askbot_settings.REPLY_BY_EMAIL and \
+ self.reputation > askbot_settings.MIN_REP_TO_POST_BY_EMAIL
+
def _assert_user_can(
user = None,
post = None, #related post (may be parent)
@@ -314,6 +329,12 @@ def _assert_user_can(
assert(error_message is not None)
raise django_exceptions.PermissionDenied(error_message)
+def user_assert_can_approve_post_revision(self, post_revision = None):
+ _assert_user_can(
+ user = self,
+ admin_or_moderator_required = True
+ )
+
def user_assert_can_unaccept_best_answer(self, answer = None):
assert getattr(answer, 'post_type', '') == 'answer'
blocked_error_message = _(
@@ -973,6 +994,7 @@ def user_post_comment(
parent_post = None,
body_text = None,
timestamp = None,
+ by_email = False
):
"""post a comment on behalf of the user
to parent_post
@@ -991,9 +1013,11 @@ def user_post_comment(
user = self,
comment = body_text,
added_at = timestamp,
+ by_email = by_email
)
parent_post.thread.invalidate_cached_data()
- award_badges_signal.send(None,
+ award_badges_signal.send(
+ None,
event = 'post_comment',
actor = self,
context_object = comment,
@@ -1001,6 +1025,23 @@ def user_post_comment(
)
return comment
+def user_post_tag_wiki(
+ self,
+ tag = None,
+ body_text = None,
+ timestamp = None
+ ):
+ """Creates a tag wiki post and assigns it
+ to the given tag. Returns the newly created post"""
+ tag_wiki_post = Post.objects.create_new_tag_wiki(
+ author = self,
+ text = body_text
+ )
+ tag.tag_wiki = tag_wiki_post
+ tag.save()
+ return tag_wiki_post
+
+
def user_post_anonymous_askbot_content(user, session_key):
"""posts any posts added just before logging in
the posts are identified by the session key, thus the second argument
@@ -1301,7 +1342,8 @@ def user_post_question(
tags = None,
wiki = False,
is_anonymous = False,
- timestamp = None
+ timestamp = None,
+ by_email = False
):
"""makes an assertion whether user can post the question
then posts it and returns the question object"""
@@ -1318,6 +1360,8 @@ def user_post_question(
if timestamp is None:
timestamp = datetime.datetime.now()
+ #todo: split this into "create thread" + "add queston", if text exists
+ #or maybe just add a blank question post anyway
thread = Thread.objects.create_new(
author = self,
title = title,
@@ -1326,6 +1370,7 @@ def user_post_question(
added_at = timestamp,
wiki = wiki,
is_anonymous = is_anonymous,
+ by_email = by_email
)
question = thread._question_post()
if question.author != self:
@@ -1334,42 +1379,70 @@ def user_post_question(
# because they set some attributes for that instance and expect them to be changed also for question.author
return question
-def user_edit_comment(self, comment_post=None, body_text = None):
+@auto_now_timestamp
+def user_edit_comment(
+ self,
+ comment_post=None,
+ body_text = None,
+ timestamp = None,
+ by_email = False
+ ):
"""apply edit to a comment, the method does not
change the comments timestamp and no signals are sent
todo: see how this can be merged with edit_post
todo: add timestamp
"""
self.assert_can_edit_comment(comment_post)
- comment_post.text = body_text
- comment_post.parse_and_save(author = self)
- comment_post.thread.invalidate_cached_data()
+ comment_post.apply_edit(
+ text = body_text,
+ edited_at = timestamp,
+ edited_by = self,
+ by_email = by_email
+ )
def user_edit_post(self,
post = None,
body_text = None,
revision_comment = None,
- timestamp = None):
+ timestamp = None,
+ by_email = False
+ ):
"""a simple method that edits post body
todo: unify it in the style of just a generic post
this requires refactoring of underlying functions
because we cannot bypass the permissions checks set within
"""
if post.post_type == 'comment':
- self.edit_comment(comment_post = post, body_text = body_text)
+ self.edit_comment(
+ comment_post = post,
+ body_text = body_text,
+ by_email = by_email
+ )
elif post.post_type == 'answer':
self.edit_answer(
answer = post,
body_text = body_text,
timestamp = timestamp,
- revision_comment = revision_comment
+ revision_comment = revision_comment,
+ by_email = by_email
)
elif post.post_type == 'question':
self.edit_question(
question = post,
body_text = body_text,
timestamp = timestamp,
- revision_comment = revision_comment
+ revision_comment = revision_comment,
+ by_email = by_email
+ )
+ elif post.post_type == 'tag_wiki':
+ post.apply_edit(
+ edited_at = timestamp,
+ edited_by = self,
+ text = body_text,
+ #todo: summary name clash in question and question revision
+ comment = revision_comment,
+ wiki = True,
+ by_email = False
)
else:
raise NotImplementedError()
@@ -1386,9 +1459,11 @@ def user_edit_question(
edit_anonymously = False,
timestamp = None,
force = False,#if True - bypass the assert
+ by_email = False
):
if force == False:
self.assert_can_edit_question(question)
+
question.apply_edit(
edited_at = timestamp,
edited_by = self,
@@ -1399,8 +1474,11 @@ def user_edit_question(
tags = tags,
wiki = wiki,
edit_anonymously = edit_anonymously,
+ by_email = by_email
)
+
question.thread.invalidate_cached_data()
+
award_badges_signal.send(None,
event = 'edit_question',
actor = self,
@@ -1416,7 +1494,8 @@ def user_edit_answer(
revision_comment = None,
wiki = False,
timestamp = None,
- force = False#if True - bypass the assert
+ force = False,#if True - bypass the assert
+ by_email = False
):
if force == False:
self.assert_can_edit_answer(answer)
@@ -1426,6 +1505,7 @@ def user_edit_answer(
text = body_text,
comment = revision_comment,
wiki = wiki,
+ by_email = by_email
)
answer.thread.invalidate_cached_data()
award_badges_signal.send(None,
@@ -1441,7 +1521,8 @@ def user_post_answer(
body_text = None,
follow = False,
wiki = False,
- timestamp = None
+ timestamp = None,
+ by_email = False
):
#todo: move this to assertion - user_assert_can_post_answer
@@ -1453,6 +1534,7 @@ def user_post_answer(
now = datetime.datetime.now()
asked = question.added_at
+ #todo: this is an assertion, must be moved out
if (now - asked < delta and self.reputation < askbot_settings.MIN_REP_TO_ANSWER_OWN_QUESTION):
diff = asked + delta - now
days = diff.days
@@ -1503,7 +1585,8 @@ def user_post_answer(
text = body_text,
added_at = timestamp,
email_notify = follow,
- wiki = wiki
+ wiki = wiki,
+ by_email = by_email
)
answer_post.thread.invalidate_cached_data()
award_badges_signal.send(None,
@@ -2058,6 +2141,27 @@ def downvote(self, post, timestamp=None, cancel=False, force = False):
)
@auto_now_timestamp
+def user_approve_post_revision(user, post_revision, timestamp = None):
+ """approves the post revision and, if necessary,
+ the parent post and threads"""
+ user.assert_can_approve_post_revision()
+
+ post_revision.approved = True
+ post_revision.approved_by = user
+ post_revision.approved_at = timestamp
+
+ post_revision.save()
+
+ post = post_revision.post
+ post.approved = True
+ post.save()
+ if post_revision.post.post_type == 'question':
+ thread = post.thread
+ thread.approved = True
+ thread.save()
+ post.thread.invalidate_cached_data()
+
+@auto_now_timestamp
def flag_post(user, post, timestamp=None, cancel=False, cancel_all = False, force = False):
if cancel_all:
# remove all flags
@@ -2173,6 +2277,21 @@ def user_update_wildcard_tag_selections(
return new_tags
+def user_edit_group_membership(self, user = None, group = None, action = None):
+ """allows one user to add another to a group
+ or remove user from group.
+
+ If when adding, the group does not exist, it will be created
+ the delete function is not symmetric, the group will remain
+ even if it becomes empty
+ """
+ if action == 'add':
+ GroupMembership.objects.get_or_create(user = user, group = group)
+ elif action == 'remove':
+ GroupMembership.objects.get(user = user, group = group).delete()
+ else:
+ raise ValueError('invalid action')
+
User.add_to_class(
'add_missing_askbot_subscriptions',
user_add_missing_askbot_subscriptions
@@ -2208,6 +2327,7 @@ User.add_to_class(
User.add_to_class('post_comment', user_post_comment)
User.add_to_class('edit_comment', user_edit_comment)
User.add_to_class('delete_post', user_delete_post)
+User.add_to_class('post_tag_wiki', user_post_tag_wiki)
User.add_to_class('visit_question', user_visit_question)
User.add_to_class('upvote', upvote)
User.add_to_class('downvote', downvote)
@@ -2231,10 +2351,12 @@ User.add_to_class('is_following_question', user_is_following_question)
User.add_to_class('mark_tags', user_mark_tags)
User.add_to_class('update_response_counts', user_update_response_counts)
User.add_to_class('can_have_strong_url', user_can_have_strong_url)
+User.add_to_class('can_reply_by_email', user_can_reply_by_email)
User.add_to_class('can_post_comment', user_can_post_comment)
User.add_to_class('is_administrator', user_is_administrator)
User.add_to_class('is_administrator_or_moderator', user_is_administrator_or_moderator)
User.add_to_class('set_admin_status', user_set_admin_status)
+User.add_to_class('edit_group_membership', user_edit_group_membership)
User.add_to_class('remove_admin_status', user_remove_admin_status)
User.add_to_class('is_moderator', user_is_moderator)
User.add_to_class('is_approved', user_is_approved)
@@ -2263,6 +2385,7 @@ User.add_to_class(
'update_wildcard_tag_selections',
user_update_wildcard_tag_selections
)
+User.add_to_class('approve_post_revision', user_approve_post_revision)
#assertions
User.add_to_class('assert_can_vote_for_post', user_assert_can_vote_for_post)
@@ -2291,9 +2414,13 @@ User.add_to_class('assert_can_delete_answer', user_assert_can_delete_answer)
User.add_to_class('assert_can_delete_question', user_assert_can_delete_question)
User.add_to_class('assert_can_accept_best_answer', user_assert_can_accept_best_answer)
User.add_to_class(
- 'assert_can_unaccept_best_answer',
- user_assert_can_unaccept_best_answer
- )
+ 'assert_can_unaccept_best_answer',
+ user_assert_can_unaccept_best_answer
+)
+User.add_to_class(
+ 'assert_can_approve_post_revision',
+ user_assert_can_approve_post_revision
+)
#todo: move this to askbot/utils ??
def format_instant_notification_email(
@@ -2346,8 +2473,8 @@ def format_instant_notification_email(
revisions = post.revisions.all()[:2]
assert(len(revisions) == 2)
content_preview = htmldiff(
- revisions[1].as_html(),
- revisions[0].as_html(),
+ revisions[1].html,
+ revisions[0].html,
ins_start = '<b><u style="background-color:#cfc">',
ins_end = '</u></b>',
del_start = '<del style="color:#600;background-color:#fcc">',
@@ -2355,34 +2482,62 @@ def format_instant_notification_email(
)
#todo: remove hardcoded style
else:
- from askbot.templatetags.extra_filters_jinja import absolutize_urls_func
- content_preview = absolutize_urls_func(post.html)
- tag_style = "white-space: nowrap; " \
- + "font-size: 11px; color: #333;" \
- + "background-color: #EEE;" \
- + "border-left: 3px solid #777;" \
- + "border-top: 1px solid #EEE;" \
- + "border-bottom: 1px solid #CCC;" \
- + "border-right: 1px solid #CCC;" \
- + "padding: 1px 8px 1px 8px;" \
- + "margin-right:3px;"
- if post.post_type == 'question':#add tags to the question
- content_preview += '<div>'
- for tag_name in post.get_tag_names():
- content_preview += '<span style="%s">%s</span>' % (tag_style, tag_name)
- content_preview += '</div>'
+ content_preview = post.format_for_email()
+
+ #add indented summaries for the parent posts
+ content_preview += post.format_for_email_as_parent_thread_summary()
+
+ content_preview += '<p>======= Full thread summary =======</p>'
+
+ content_preview += post.thread.format_for_email()
+
+ if post.is_comment():
+ if update_type.endswith('update'):
+ user_action = _('%(user)s edited a %(post_link)s.')
+ else:
+ user_action = _('%(user)s posted a %(post_link)s')
+ elif post.is_answer():
+ if update_type.endswith('update'):
+ user_action = _('%(user)s edited an %(post_link)s.')
+ else:
+ user_action = _('%(user)s posted an %(post_link)s.')
+ elif post.is_question():
+ if update_type.endswith('update'):
+ user_action = _('%(user)s edited a %(post_link)s.')
+ else:
+ user_action = _('%(user)s posted a %(post_link)s.')
+ else:
+ raise ValueError('unrecognized post type')
+
+ post_url = strip_path(site_url) + post.get_absolute_url()
+ user_url = strip_path(site_url) + from_user.get_absolute_url()
+ user_action = user_action % {
+ 'user': '<a href="%s">%s</a>' % (user_url, from_user.username),
+ 'post_link': '<a href="%s">%s</a>' % (post_url, _(post.post_type))
+ }
+ can_reply = to_user.can_reply_by_email()
+
+ if can_reply:
+ reply_separator = const.REPLY_SEPARATOR_TEMPLATE % {
+ 'user_action': user_action,
+ 'instruction': _('To reply, PLEASE WRITE ABOVE THIS LINE.')
+ }
+ else:
+ reply_separator = user_action
+
update_data = {
'update_author_name': from_user.username,
'receiving_user_name': to_user.username,
'receiving_user_karma': to_user.reputation,
'reply_by_email_karma_threshold': askbot_settings.MIN_REP_TO_POST_BY_EMAIL,
- 'can_reply': to_user.reputation > askbot_settings.MIN_REP_TO_POST_BY_EMAIL,
+ 'can_reply': can_reply,
'content_preview': content_preview,#post.get_snippet()
'update_type': update_type,
- 'post_url': strip_path(site_url) + post.get_absolute_url(),
+ 'post_url': post_url,
'origin_post_title': origin_post.thread.title,
'user_subscriptions_url': user_subscriptions_url,
+ 'reply_separator': reply_separator
}
subject_line = _('"%(title)s"') % {'title': origin_post.thread.title}
return subject_line, template.render(Context(update_data))
@@ -2398,6 +2553,8 @@ def send_instant_notifications_about_activity_in_post(
newly mentioned users are carried through to reduce
database hits
"""
+ if askbot_settings.ENABLE_CONTENT_MODERATION and post.approved == False:
+ return
if recipients is None:
return
@@ -2408,23 +2565,18 @@ def send_instant_notifications_about_activity_in_post(
return
from askbot.skins.loaders import get_template
- template = get_template('instant_notification.html')
-
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()
for user in recipients:
-
- if askbot_settings.REPLY_BY_EMAIL:
- template = get_template('instant_notification_reply_by_email.html')
subject_line, body_text = format_instant_notification_email(
to_user = user,
from_user = update_activity.user,
post = post,
update_type = update_type,
- template = template,
+ template = get_template('instant_notification.html')
)
#todo: this could be packaged as an "action" - a bundle
@@ -2448,9 +2600,6 @@ def send_instant_notifications_about_activity_in_post(
headers = headers
)
-
-
-
#todo: move to utils
def calculate_gravatar_hash(instance, **kwargs):
"""Calculates a User's gravatar hash from their email address."""
@@ -2471,7 +2620,15 @@ def record_post_update_activity(
):
"""called upon signal askbot.models.signals.post_updated
which is sent at the end of save() method in posts
+
+ this handler will set notifications about the post
"""
+ if post.needs_moderation():
+ #do not give notifications yet
+ #todo: it is possible here to trigger
+ #moderation email alerts
+ return
+
assert(timestamp != None)
assert(updated_by != None)
if newly_mentioned_users is None:
@@ -2532,6 +2689,8 @@ def notify_award_message(instance, created, **kwargs):
"""
Notify users when they have been awarded badges by using Django message.
"""
+ if askbot_settings.BADGES_MODE != 'public':
+ return
if created:
user = instance.user
@@ -2659,10 +2818,7 @@ def record_flag_offensive(instance, mark_by, **kwargs):
# recipients = instance.get_author_list(
# exclude_list = [mark_by]
# )
- recipients = User.objects.filter(
- models.Q(is_superuser=True) | models.Q(status='m')
- )
- activity.add_recipients(recipients)
+ activity.add_recipients(get_admins_and_moderators())
def remove_flag_offensive(instance, mark_by, **kwargs):
"Remove flagging activity"
@@ -2769,7 +2925,6 @@ def set_user_avatar_type_flag(instance, created, **kwargs):
def update_user_avatar_type_flag(instance, **kwargs):
instance.user.update_avatar_type()
-
def make_admin_if_first_user(instance, **kwargs):
"""first user automatically becomes an administrator
the function is run only once in the interpreter session
@@ -2787,9 +2942,22 @@ def make_admin_if_first_user(instance, **kwargs):
instance.set_admin_status()
cache.cache.set('admin-created', True)
+def place_post_revision_on_moderation_queue(instance, **kwargs):
+ """`instance` is post revision, because we must
+ be able to moderate all the revisions, if necessary,
+ in order to avoid people getting the post past the moderation
+ then make some evil edit.
+ """
+ if instance.needs_moderation():
+ instance.place_on_moderation_queue()
+
#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)
+django_signals.post_save.connect(
+ place_post_revision_on_moderation_queue,
+ sender=PostRevision
+)
django_signals.post_save.connect(add_missing_subscriptions, sender=User)
django_signals.post_save.connect(record_award_event, sender=Award)
django_signals.post_save.connect(notify_award_message, sender=Award)
@@ -2847,10 +3015,13 @@ __all__ = [
'Activity',
'ActivityAuditStatus',
'EmailFeedSetting',
+ 'GroupMembership',
+ 'GroupProfile',
'User',
'ReplyAddress',
- 'get_model'
+ 'get_model',
+ 'get_admins_and_moderators'
]
diff --git a/askbot/models/post.py b/askbot/models/post.py
index 4ed528ea..e6e7ec9d 100644
--- a/askbot/models/post.py
+++ b/askbot/models/post.py
@@ -13,6 +13,7 @@ from django.core import urlresolvers
from django.db import models
from django.utils import html as html_utils
from django.utils.translation import ugettext as _
+from django.utils.translation import ungettext
from django.utils.http import urlquote as django_urlquote
from django.core import exceptions as django_exceptions
from django.core.exceptions import ValidationError
@@ -34,7 +35,6 @@ from askbot.models.base import BaseQuerySetManager
from askbot.utils.diff import textDiff as htmldiff
from askbot.utils import mysql
-
class PostQuerySet(models.query.QuerySet):
"""
Custom query set subclass for :class:`~askbot.models.Post`
@@ -148,44 +148,94 @@ class PostManager(BaseQuerySetManager):
def get_comments(self):
return self.filter(post_type='comment')
- def create_new_answer(self, thread, author, added_at, text, wiki=False, email_notify=False):
+ def create_new_tag_wiki(self, text = None, author = None):
+ return self.create_new(
+ None,#this post type is threadless
+ author,
+ datetime.datetime.now(),
+ text,
+ wiki = True,
+ post_type = 'tag_wiki'
+ )
+
+ def create_new(
+ self,
+ thread,
+ author,
+ added_at,
+ text,
+ parent = None,
+ wiki = False,
+ email_notify = False,
+ post_type = None,
+ by_email = False
+ ):
# TODO: Some of this code will go to Post.objects.create_new
- answer = Post(
- post_type='answer',
- thread=thread,
- author=author,
- added_at=added_at,
- wiki=wiki,
- text=text,
+
+ assert(post_type in const.POST_TYPES)
+
+ post = Post(
+ post_type = post_type,
+ thread = thread,
+ parent = parent,
+ author = author,
+ added_at = added_at,
+ wiki = wiki,
+ text = text,
#.html field is denormalized by the save() call
)
- if answer.wiki:
- answer.last_edited_by = answer.author
- answer.last_edited_at = added_at
- answer.wikified_at = added_at
- answer.parse_and_save(author=author)
+ if post.wiki:
+ post.last_edited_by = post.author
+ post.last_edited_at = added_at
+ post.wikified_at = added_at
+
+ post.parse_and_save(author=author)
- answer.add_revision(
- author=author,
- revised_at=added_at,
- text=text,
+ post.add_revision(
+ author = author,
+ revised_at = added_at,
+ text = text,
comment = const.POST_STATUS['default_version'],
+ by_email = by_email
)
-
- #update thread data
- thread.answer_count +=1
- thread.save()
- thread.set_last_activity(last_activity_at=added_at, last_activity_by=author) # this should be here because it regenerates cached thread summary html
-
+
+ return post
+
+ #todo: instead of this, have Thread.add_answer()
+ def create_new_answer(
+ self,
+ thread,
+ author,
+ added_at,
+ text,
+ wiki = False,
+ email_notify = False,
+ by_email = False
+ ):
+ answer = self.create_new(
+ thread,
+ author,
+ added_at,
+ text,
+ wiki = wiki,
+ post_type = 'answer',
+ by_email = by_email
+ )
#set notification/delete
if email_notify:
thread.followed_by.add(author)
else:
thread.followed_by.remove(author)
+ #update thread data
+ #todo: this totally belongs to some `Thread` class method
+ thread.answer_count += 1
+ thread.save()
+ thread.set_last_activity(last_activity_at=added_at, last_activity_by=author) # this should be here because it regenerates cached thread summary html
return answer
+
def precache_comments(self, for_posts, visitor):
"""
Fetches comments for given posts, and stores them in post._cached_comments
@@ -237,11 +287,16 @@ class Post(models.Model):
old_comment_id = models.PositiveIntegerField(null=True, blank=True, default=None, unique=True)
parent = models.ForeignKey('Post', blank=True, null=True, related_name='comments') # Answer or Question for Comment
- thread = models.ForeignKey('Thread', related_name='posts')
+ thread = models.ForeignKey('Thread', blank=True, null=True, default = None, related_name='posts')
author = models.ForeignKey(User, related_name='posts')
added_at = models.DateTimeField(default=datetime.datetime.now)
+ #denormalized data: the core approval of the posts is made
+ #in the revisions. In the revisions there is more data about
+ #approvals - by whom and when
+ approved = models.BooleanField(default=True, db_index=True)
+
deleted = models.BooleanField(default=False, db_index=True)
deleted_at = models.DateTimeField(null=True, blank=True)
deleted_by = models.ForeignKey(User, null=True, blank=True, related_name='deleted_posts')
@@ -301,7 +356,7 @@ class Post(models.Model):
removed_mentions - list of mention <Activity> objects - for removed ones
"""
- if post.is_answer() or post.is_question():
+ if post.post_type in ('question', 'answer', 'tag_wiki'):
_urlize = False
_use_markdown = True
_escape_html = False #markdow does the escaping
@@ -398,7 +453,7 @@ class Post(models.Model):
#a hack allowing to save denormalized .summary field for questions
if hasattr(post, 'summary'):
- post.summary = strip_tags(post.html)[:120]
+ post.summary = post.get_snippet()
#delete removed mentions
for rm in removed_mentions:
@@ -452,6 +507,12 @@ class Post(models.Model):
def is_comment(self):
return self.post_type == 'comment'
+ def is_tag_wiki(self):
+ return self.post_type == 'tag_wiki'
+
+ def needs_moderation(self):
+ return self.approved == False
+
def get_absolute_url(self, no_slug = False, question_post=None, thread=None):
from askbot.utils.slug import slugify
if not hasattr(self, '_thread_cache') and thread:
@@ -535,10 +596,98 @@ class Post(models.Model):
return slugify(self.thread.title)
slug = property(_get_slug)
- def get_snippet(self):
+ def get_snippet(self, max_length = 120):
"""returns an abbreviated snippet of the content
"""
- return html_utils.strip_tags(self.html)[:120] + ' ...'
+ return html_utils.strip_tags(self.html)[:max_length] + ' ...'
+
+ def format_tags_for_email(self):
+ """formats tags of the question post for email"""
+ tag_style = "white-space: nowrap; " \
+ + "font-size: 11px; color: #333;" \
+ + "background-color: #EEE;" \
+ + "border-left: 3px solid #777;" \
+ + "border-top: 1px solid #EEE;" \
+ + "border-bottom: 1px solid #CCC;" \
+ + "border-right: 1px solid #CCC;" \
+ + "padding: 1px 8px 1px 8px;" \
+ + "margin-right:3px;"
+ output = '<div>'
+ for tag_name in self.get_tag_names():
+ output += '<span style="%s">%s</span>' % (tag_style, tag_name)
+ output += '</div>'
+ return output
+
+ def format_for_email(self, quote_level = 0):
+ """format post for the output in email,
+ if quote_level > 0, the post will be indented that number of times
+ """
+ from askbot.templatetags.extra_filters_jinja import absolutize_urls_func
+ output = ''
+ if self.post_type == 'question':
+ output += '<b>%s</b><br/>' % self.thread.title
+
+ output += absolutize_urls_func(self.html)
+ if self.post_type == 'question':#add tags to the question
+ output += self.format_tags_for_email()
+ quote_style = 'padding-left:5px; border-left: 2px solid #aaa;'
+ while quote_level > 0:
+ quote_level = quote_level - 1
+ output = '<div style="%s">%s</div>' % (quote_style, output)
+ return output
+
+ def format_for_email_as_parent_thread_summary(self):
+ """format for email as summary of parent posts
+ all the way to the original question"""
+ quote_level = 0
+ current_post = self
+ output = ''
+ while True:
+ parent_post = current_post.get_parent_post()
+ if parent_post is None:
+ break
+ quote_level += 1
+ output += _(
+ 'In reply to %(user)s %(post)s of %(date)s<br/>'
+ ) % {
+ 'user': parent_post.author.username,
+ 'post': _(parent_post.post_type),
+ 'date': parent_post.added_at.strftime(const.DATETIME_FORMAT)
+ }
+ output += parent_post.format_for_email(quote_level = quote_level)
+ current_post = parent_post
+ return output
+
+ def format_for_email_as_metadata(self):
+ output = _(
+ 'Posted by %(user)s on %(date)s'
+ ) % {
+ 'user': self.author.username,
+ 'date': self.added_at.strftime(const.DATETIME_FORMAT)
+ }
+ return '<p>%s</p>' % output
+
+ def format_for_email_as_subthread(self):
+ """outputs question or answer and all it's comments
+ returns empty string for all other post types
+ """
+ if self.post_type in ('question', 'answer'):
+ output = self.format_for_email_as_metadata()
+ output += self.format_for_email()
+ comments = self.get_cached_comments()
+ if comments:
+ comments_heading = ungettext(
+ '%(count)d comment:',
+ '%(count)d comments:',
+ len(comments)
+ ) % {'count': len(comments)}
+ output += '<p>%s</p>' % comments_heading
+ for comment in comments:
+ output += comment.format_for_email_as_metadata()
+ output += comment.format_for_email(quote_level = 1)
+ return output
+ else:
+ return ''
def set_cached_comments(self, comments):
"""caches comments in the lifetime of the object
@@ -553,22 +702,27 @@ class Post(models.Model):
self._cached_comments = list()
return self._cached_comments
- def add_comment(self, comment=None, user=None, added_at=None):
+ def add_comment(
+ self,
+ comment=None,
+ user=None,
+ added_at=None,
+ by_email = False):
+
if added_at is None:
added_at = datetime.datetime.now()
- if None in (comment ,user):
+ if None in (comment, user):
raise Exception('arguments comment and user are required')
- from askbot.models import Post
- comment = Post(
- post_type='comment',
- thread=self.thread,
- parent=self,
- text=comment,
- author=user,
- added_at=added_at
- )
- comment.parse_and_save(author = user)
+ comment_post = self.__class__.objects.create_new(
+ self.thread,
+ user,
+ added_at,
+ comment,
+ parent = self,
+ post_type = 'comment',
+ by_email = by_email
+ )
self.comment_count = self.comment_count + 1
self.save()
@@ -588,7 +742,7 @@ class Post(models.Model):
# origin_post.last_activity_by = user
# origin_post.save()
- return comment
+ return comment_post
def get_global_tag_based_subscribers(
self,
@@ -883,6 +1037,8 @@ class Post(models.Model):
mentioned_users=mentioned_users,
exclude_list=exclude_list
)
+ elif self.is_tag_wiki():
+ return list()
raise NotImplementedError
def get_latest_revision(self):
@@ -965,9 +1121,21 @@ class Post(models.Model):
def tagname_meta_generator(self):
return u','.join([unicode(tag) for tag in self.get_tag_names()])
+ def get_parent_post(self):
+ """returns parent post or None
+ if there is no parent, as it is in the case of question post"""
+ if self.post_type == 'comment':
+ return self.parent
+ elif self.post_type == 'answer':
+ return self.get_origin_post()
+ else:
+ return None
+
def get_origin_post(self):
- if self.post_type == 'question':
+ if self.is_question():
return self
+ if self.is_tag_wiki():
+ return None
else:
return self.thread._question_post()
@@ -1150,14 +1318,27 @@ class Post(models.Model):
return const.TYPE_ACTIVITY_COMMENT_QUESTION, self
elif self.parent.post_type == 'answer':
return const.TYPE_ACTIVITY_COMMENT_ANSWER, self
+ elif self.is_tag_wiki():
+ if created:
+ return const.TYPE_ACTIVITY_CREATE_TAG_WIKI, self
+ else:
+ return const.TYPE_ACTIVITY_UPDATE_TAG_WIKI, self
raise NotImplementedError
def get_tag_names(self):
return self.thread.get_tag_names()
- def _answer__apply_edit(self, edited_at=None, edited_by=None, text=None, comment=None, wiki=False):
-
+ def __apply_edit(
+ self,
+ edited_at = None,
+ edited_by = None,
+ text = None,
+ comment = None,
+ wiki = False,
+ edit_anonymously = False,
+ by_email = False
+ ):
if text is None:
text = self.get_latest_revision().text
if edited_at is None:
@@ -1169,49 +1350,63 @@ class Post(models.Model):
self.last_edited_by = edited_by
#self.html is denormalized in save()
self.text = text
- #todo: bug wiki has no effect here
+ self.is_anonymous = edit_anonymously
+
+ #wiki is an eternal trap whence there is no exit
+ if self.wiki == False and wiki == True:
+ self.wiki = True
#must add revision before saving the answer
self.add_revision(
author = edited_by,
revised_at = edited_at,
text = text,
- comment = comment
+ comment = comment,
+ by_email = by_email
)
self.parse_and_save(author = edited_by)
+ def _answer__apply_edit(
+ self,
+ edited_at = None,
+ edited_by = None,
+ text = None,
+ comment = None,
+ wiki = False,
+ by_email = False
+ ):
+
+ self.__apply_edit(
+ edited_at = edited_at,
+ edited_by = edited_by,
+ text = text,
+ comment = comment,
+ wiki = wiki,
+ by_email = by_email
+ )
+ if edited_at is None:
+ edited_at = datetime.datetime.now()
self.thread.set_last_activity(last_activity_at=edited_at, last_activity_by=edited_by)
def _question__apply_edit(self, edited_at=None, edited_by=None, title=None,\
text=None, comment=None, tags=None, wiki=False,\
- edit_anonymously = False):
+ edit_anonymously = False,
+ by_email = False
+ ):
+ #todo: the thread editing should happen outside of this
+ #method, then we'll be able to unify all the *__apply_edit
+ #methods
latest_revision = self.get_latest_revision()
#a hack to allow partial edits - important for SE loader
if title is None:
title = self.thread.title
- if text is None:
- text = latest_revision.text
if tags is None:
tags = latest_revision.tagnames
-
- if edited_by is None:
- raise Exception('parameter edited_by is required')
-
if edited_at is None:
edited_at = datetime.datetime.now()
- # Update the Question itself
- self.last_edited_at = edited_at
- self.last_edited_by = edited_by
- self.text = text
- self.is_anonymous = edit_anonymously
-
- #wiki is an eternal trap whence there is no exit
- if self.wiki == False and wiki == True:
- self.wiki = True
-
# Update the Question tag associations
if latest_revision.tagnames != tags:
self.thread.update_tags(tagnames = tags, user = edited_by, timestamp = edited_at)
@@ -1220,27 +1415,39 @@ class Post(models.Model):
self.thread.tagnames = tags
self.thread.save()
- # Create a new revision
- self.add_revision( # has to be called AFTER updating the thread, otherwise it doesn't see new tags and the new title
- author = edited_by,
+ self.__apply_edit(
+ edited_at = edited_at,
+ edited_by = edited_by,
text = text,
- revised_at = edited_at,
- is_anonymous = edit_anonymously,
comment = comment,
+ wiki = wiki,
+ edit_anonymously = edit_anonymously,
+ by_email = by_email
)
- self.parse_and_save(author = edited_by)
-
self.thread.set_last_activity(last_activity_at=edited_at, last_activity_by=edited_by)
- def apply_edit(self, *kargs, **kwargs):
+ def apply_edit(self, *args, **kwargs):
+ #todo: unify this, here we have unnecessary indirection
+ #the question__apply_edit function is backwards:
+ #the title edit and tag edit should apply to thread
+ #not the question post
if self.is_answer():
- return self._answer__apply_edit(*kargs, **kwargs)
+ return self._answer__apply_edit(*args, **kwargs)
elif self.is_question():
- return self._question__apply_edit(*kargs, **kwargs)
+ return self._question__apply_edit(*args, **kwargs)
+ elif self.is_tag_wiki() or self.is_comment():
+ return self.__apply_edit(*args, **kwargs)
raise NotImplementedError
- def _answer__add_revision(self, author=None, revised_at=None, text=None, comment=None):
+ def __add_revision(
+ self,
+ author = None,
+ revised_at = None,
+ text = None,
+ comment = None,
+ by_email = False
+ ):
#todo: this may be identical to Question.add_revision
if None in (author, revised_at, text):
raise Exception('arguments author, revised_at and text are required')
@@ -1252,12 +1459,13 @@ class Post(models.Model):
comment = 'No.%s Revision' % rev_no
from askbot.models.post import PostRevision
return PostRevision.objects.create_answer_revision(
- post=self,
- author=author,
- revised_at=revised_at,
- text=text,
- summary=comment,
- revision=rev_no
+ post = self,
+ author = author,
+ revised_at = revised_at,
+ text = text,
+ summary = comment,
+ revision = rev_no,
+ by_email = by_email
)
def _question__add_revision(
@@ -1266,7 +1474,8 @@ class Post(models.Model):
is_anonymous = False,
text = None,
comment = None,
- revised_at = None
+ revised_at = None,
+ by_email = False
):
if None in (author, text):
raise Exception('author, text and comment are required arguments')
@@ -1287,12 +1496,14 @@ class Post(models.Model):
revised_at = revised_at,
tagnames = self.thread.tagnames,
summary = comment,
- text = text
+ text = text,
+ by_email = by_email
)
def add_revision(self, *kargs, **kwargs):
- if self.is_answer():
- return self._answer__add_revision(*kargs, **kwargs)
+ #todo: unify these
+ if self.post_type in ('answer', 'comment', 'tag_wiki'):
+ return self.__add_revision(*kargs, **kwargs)
elif self.is_question():
return self._question__add_revision(*kargs, **kwargs)
raise NotImplementedError
@@ -1365,12 +1576,15 @@ class Post(models.Model):
def get_response_receivers(self, exclude_list = None):
+ """returns a list of response receiving users"""
if self.is_answer():
return self._answer__get_response_receivers(exclude_list)
elif self.is_question():
return self._question__get_response_receivers(exclude_list)
elif self.is_comment():
return self._comment__get_response_receivers(exclude_list)
+ elif self.is_tag_wiki():
+ return list()#todo: who should get these?
raise NotImplementedError
def get_question_title(self):
@@ -1471,6 +1685,8 @@ class PostRevision(models.Model):
post = models.ForeignKey('askbot.Post', related_name='revisions', null=True, blank=True)
+ #todo: remove this field, as revision type is determined by the
+ #Post.post_type revision_type is a useless field
revision_type = models.SmallIntegerField(choices=REVISION_TYPE_CHOICES) # TODO: remove as we have Post now
revision = models.PositiveIntegerField()
@@ -1479,6 +1695,12 @@ class PostRevision(models.Model):
summary = models.CharField(max_length=300, blank=True)
text = models.TextField()
+ approved = models.BooleanField(default=False, db_index=True)
+ approved_by = models.ForeignKey(User, null = True, blank = True)
+ approved_at = models.DateTimeField(null= True, blank = True)
+
+ by_email = models.BooleanField(default=False)#true, if edited by email
+
# Question-specific fields
title = models.CharField(max_length=300, blank=True, default='')
tagnames = models.CharField(max_length=125, blank=True, default='')
@@ -1494,6 +1716,83 @@ class PostRevision(models.Model):
ordering = ('-revision',)
app_label = 'askbot'
+ def needs_moderation(self):
+ """``True`` if post needs moderation"""
+ if askbot_settings.ENABLE_CONTENT_MODERATION:
+ #todo: needs a lot of details
+ if self.author.is_administrator_or_moderator():
+ return False
+ if self.approved:
+ return False
+ return True
+ return False
+
+ def place_on_moderation_queue(self):
+ """If revision is the first one,
+ keeps the post invisible until the revision
+ is aprroved.
+ If the revision is an edit, will autoapprove
+ but will still add it to the moderation queue.
+
+ Eventually we might find a way to moderate every
+ edit as well."""
+ #this is run on "post-save" so for a new post
+ #we'll have just one revision
+ if self.post.revisions.count() == 1:
+ activity_type = const.TYPE_ACTIVITY_MODERATED_NEW_POST
+
+ self.approved = False
+ self.approved_by = None
+ self.approved_at = None
+
+ self.post.approved = False
+ self.post.save()
+
+ if self.post.is_question():
+ self.post.thread.approved = False
+ self.post.thread.save()
+ #above changes will hide post from the public display
+ if self.by_email:
+ from askbot.utils.mail import send_mail
+ email_context = {
+ 'site': askbot_settings.APP_SHORT_NAME
+ }
+ body_text = _(
+ 'Thank you for your post to %(site)s. '
+ 'It will be published after the moderators review.'
+ ) % email_context
+ send_mail(
+ subject_line = _('your post to %(site)s') % email_context,
+ body_text = body_text,
+ recipient_list = [self.author.email,],
+ )
+
+ else:
+ message = _(
+ 'Your post was placed on the moderation queue '
+ 'and will be published after the moderator approval.'
+ )
+ self.author.message_set.create(message = message)
+ else:
+ #In this case, for now we just flag the edit
+ #for the moderators.
+ #Ideally we'd need to hide the edit itself,
+ #but the complication is that when we have more
+ #than one edit in a row and then we'll need to deal with
+ #merging multiple edits. We don't have a solution for this yet.
+ activity_type = const.TYPE_ACTIVITY_MODERATED_POST_EDIT
+
+ from askbot.models import Activity, get_admins_and_moderators
+ activity = Activity(
+ user = self.author,
+ content_object = self,
+ activity_type = activity_type,
+ question = self.get_origin_post()
+ )
+ activity.save()
+ #todo: make this group-sensitive
+ activity.add_recipients(get_admins_and_moderators())
+
def revision_type_str(self):
return self.REVISION_TYPE_CHOICES_DICT[self.revision_type]
@@ -1533,15 +1832,20 @@ class PostRevision(models.Model):
@models.permalink
def get_absolute_url(self):
if self.is_question_revision():
- return 'question_revisions', (self.question.id,), {}
+ return 'question_revisions', (self.post.id,), {}
elif self.is_answer_revision():
- return 'answer_revisions', (), {'id':self.answer.id}
+ return 'answer_revisions', (), {'id':self.post.id}
def get_question_title(self):
#INFO: ack-grepping shows that it's only used for Questions, so there's no code for Answers
return self.question.thread.title
- def as_html(self, **kwargs):
+ def get_origin_post(self):
+ """same as Post.get_origin_post()"""
+ return self.post.get_origin_post()
+
+ @property
+ def html(self, **kwargs):
markdowner = markup.get_parser()
sanitized_html = sanitize_html(markdowner.convert(self.text))
@@ -1552,3 +1856,7 @@ class PostRevision(models.Model):
}
elif self.is_answer_revision():
return sanitized_html
+
+ def get_snippet(self, max_length = 120):
+ """same as Post.get_snippet"""
+ return html_utils.strip_tags(self.html)[:max_length] + '...'
diff --git a/askbot/models/question.py b/askbot/models/question.py
index 8c61385c..21cf21d0 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -9,6 +9,7 @@ from django.core import cache # import cache, not from cache import cache, to b
from django.core.urlresolvers import reverse
from django.utils.hashcompat import md5_constructor
from django.utils.translation import ugettext as _
+from django.utils.translation import ungettext
import askbot
import askbot.conf
@@ -61,7 +62,17 @@ class ThreadManager(models.Manager):
def create(self, *args, **kwargs):
raise NotImplementedError
- def create_new(self, title, author, added_at, wiki, text, tagnames=None, is_anonymous=False):
+ def create_new(
+ self,
+ title,
+ author,
+ added_at,
+ wiki,
+ text,
+ tagnames = None,
+ is_anonymous = False,
+ by_email = False
+ ):
# TODO: Some of this code will go to Post.objects.create_new
thread = super(
@@ -74,6 +85,7 @@ class ThreadManager(models.Manager):
last_activity_by=author
)
+ #todo: code below looks like ``Post.objects.create_new()``
question = Post(
post_type='question',
thread=thread,
@@ -102,6 +114,7 @@ class ThreadManager(models.Manager):
text = text,
comment = const.POST_STATUS['default_version'],
revised_at = added_at,
+ by_email = by_email
)
# INFO: Question has to be saved before update_tags() is called
@@ -155,7 +168,13 @@ class ThreadManager(models.Manager):
from askbot.conf import settings as askbot_settings # Avoid circular import
# TODO: add a possibility to see deleted questions
- qs = self.filter(posts__post_type='question', posts__deleted=False) # (***) brings `askbot_post` into the SQL query, see the ordering section below
+ qs = self.filter(
+ posts__post_type='question',
+ posts__deleted=False,
+ ) # (***) brings `askbot_post` into the SQL query, see the ordering section below
+
+ if askbot_settings.ENABLE_CONTENT_MODERATION:
+ qs = qs.filter(approved = True)
meta_data = {}
@@ -331,6 +350,11 @@ class Thread(models.Model):
blank=True
)
+ #denormalized data: the core approval of the posts is made
+ #in the revisions. In the revisions there is more data about
+ #approvals - by whom and when
+ approved = models.BooleanField(default=True, db_index=True)
+
accepted_answer = models.ForeignKey(Post, null=True, blank=True, related_name='+')
answer_accepted_at = models.DateTimeField(null=True, blank=True)
added_at = models.DateTimeField(default = datetime.datetime.now)
@@ -416,6 +440,21 @@ class Thread(models.Model):
else:
return self.title
+ def format_for_email(self):
+ """experimental function: output entire thread for email"""
+ question, answers, junk = self.get_cached_post_data()
+ output = question.format_for_email_as_subthread()
+ if answers:
+ answer_heading = ungettext(
+ '%(count)d answer:',
+ '%(count)d answers:',
+ len(answers)
+ ) % {'count': len(answers)}
+ output += '<p>%s</p>' % answer_heading
+ for answer in answers:
+ output += answer.format_for_email_as_subthread()
+ return output
+
def tagname_meta_generator(self):
return u','.join([unicode(tag) for tag in self.get_tag_names()])
@@ -465,7 +504,7 @@ class Thread(models.Model):
self.invalidate_cached_post_data()
self.invalidate_cached_thread_content_fragment()
- def get_cached_post_data(self, sort_method = None):
+ def get_cached_post_data(self, sort_method = 'votes'):
"""returns cached post data, as calculated by
the method get_post_data()"""
key = self.get_post_data_cache_key(sort_method)
@@ -475,7 +514,7 @@ class Thread(models.Model):
cache.cache.set(key, post_data, const.LONG_TIME)
return post_data
- def get_post_data(self, sort_method = None):
+ def get_post_data(self, sort_method = 'votes'):
"""returns question, answers as list and a list of post ids
for the given thread
the returned posts are pre-stuffed with the comments
@@ -484,9 +523,9 @@ class Thread(models.Model):
"""
thread_posts = self.posts.all().order_by(
{
- "latest":"-added_at",
- "oldest":"added_at",
- "votes":"-score"
+ 'latest':'-added_at',
+ 'oldest':'added_at',
+ 'votes':'-score'
}[sort_method]
)
#1) collect question, answer and comment posts and list of post id's
@@ -499,6 +538,8 @@ class Thread(models.Model):
#pass through only deleted question posts
if post.deleted and post.post_type != 'question':
continue
+ if post.approved == False:#hide posts on the moderation queue
+ continue
post_to_author[post.id] = post.author_id
diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py
index 26c53999..d329d38b 100644
--- a/askbot/models/reply_by_email.py
+++ b/askbot/models/reply_by_email.py
@@ -3,13 +3,14 @@ import random
import string
from django.db import models
from django.contrib.auth.models import User
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext as _
from askbot.models.post import Post
from askbot.models.base import BaseQuerySetManager
from askbot.conf import settings as askbot_settings
from askbot.utils import mail
class ReplyAddressManager(BaseQuerySetManager):
+ """A manager for the :class:`ReplyAddress` model"""
def get_unused(self, address, allowed_from_email):
return self.get(
@@ -19,6 +20,7 @@ class ReplyAddressManager(BaseQuerySetManager):
)
def create_new(self, post, user):
+ """creates a new reply address"""
reply_address = ReplyAddress(
post = post,
user = user,
@@ -34,6 +36,8 @@ class ReplyAddressManager(BaseQuerySetManager):
class ReplyAddress(models.Model):
+ """Stores a reply address for the post
+ and the user"""
address = models.CharField(max_length = 25, unique = True)
post = models.ForeignKey(
Post,
@@ -60,35 +64,53 @@ class ReplyAddress(models.Model):
"""True if was used"""
return self.used_at != None
- def edit_post(self, content, attachments = None):
+ def edit_post(self, parts):
"""edits the created post upon repeated response
to the same address"""
assert self.was_used == True
- content += mail.process_attachments(attachments)
+ content, stored_files = mail.process_parts(parts)
self.user.edit_post(
post = self.response_post,
body_text = content,
- revision_comment = _('edited by email')
+ revision_comment = _('edited by email'),
+ by_email = True
)
self.response_post.thread.invalidate_cached_data()
- def create_reply(self, content, attachments = None):
+ def create_reply(self, parts):
"""creates a reply to the post which was emailed
to the user
"""
result = None
- content += mail.process_attachments(attachments)
+ #todo: delete stored files if this function fails
+ content, stored_files = mail.process_parts(parts)
if self.post.post_type == 'answer':
- result = self.user.post_comment(self.post, content)
+ result = self.user.post_comment(
+ self.post,
+ content,
+ by_email = True
+ )
elif self.post.post_type == 'question':
wordcount = len(content)/6#this is a simplistic hack
if wordcount > askbot_settings.MIN_WORDS_FOR_ANSWER_BY_EMAIL:
- result = self.user.post_answer(self.post, content)
+ result = self.user.post_answer(
+ self.post,
+ content,
+ by_email = True
+ )
else:
- result = self.user.post_comment(self.post, content)
+ result = self.user.post_comment(
+ self.post,
+ content,
+ by_email = True
+ )
elif self.post.post_type == 'comment':
- result = self.user.post_comment(self.post.parent, content)
+ result = self.user.post_comment(
+ self.post.parent,
+ content,
+ by_email = True
+ )
result.thread.invalidate_cached_data()
self.response_post = result
self.used_at = datetime.now()
diff --git a/askbot/models/tag.py b/askbot/models/tag.py
index a13de661..06171067 100644
--- a/askbot/models/tag.py
+++ b/askbot/models/tag.py
@@ -82,6 +82,53 @@ class TagManager(BaseQuerySetManager):
def get_query_set(self):
return TagQuerySet(self.model)
+#todo: implement this
+#class GroupTagQuerySet(models.query.QuerySet):
+# """Custom query set for the group"""
+# def __init__(self, model):
+def clean_group_name(name):
+ """group names allow spaces,
+ tag names do not, so we use this method
+ to replace spaces with dashes"""
+ return re.sub('\s+', '-', name.strip())
+
+class GroupTagManager(TagManager):
+ """manager for group tags"""
+
+# def get_query_set(self):
+# return GroupTagQuerySet(self.model)
+
+ def get_or_create(self, group_name = None, user = None):
+ """creates a group tag or finds one, if exists"""
+ #todo: here we might fill out the group profile
+
+ #replace spaces with dashes
+ group_name = clean_group_name(group_name)
+ try:
+ tag = self.get(name = group_name)
+ except self.model.DoesNotExist:
+ tag = self.model(name = group_name, created_by = user)
+ tag.save()
+ from askbot.models.user import GroupProfile
+ group_profile = GroupProfile(group_tag = tag)
+ group_profile.save()
+ return tag
+
+ #todo: maybe move this to query set
+ def get_for_user(self, user = None):
+ return self.filter(user_memberships__user = user)
+
+ #todo: remove this when the custom query set is done
+ def get_all(self):
+ return self.annotate(
+ member_count = models.Count('user_memberships')
+ ).filter(
+ member_count__gt = 0
+ )
+
+ def get_by_name(self, group_name = None):
+ return self.get(name = clean_group_name(group_name))
+
class Tag(models.Model):
name = models.CharField(max_length=255, unique=True)
created_by = models.ForeignKey(User, related_name='created_tags')
@@ -92,7 +139,14 @@ class Tag(models.Model):
deleted_at = models.DateTimeField(null=True, blank=True)
deleted_by = models.ForeignKey(User, null=True, blank=True, related_name='deleted_tags')
+ tag_wiki = models.OneToOneField(
+ 'Post',
+ null=True,
+ related_name = 'described_tag'
+ )
+
objects = TagManager()
+ group_tags = GroupTagManager()
class Meta:
app_label = 'askbot'
diff --git a/askbot/models/user.py b/askbot/models/user.py
index 6f27cbf3..3c1e0ded 100644
--- a/askbot/models/user.py
+++ b/askbot/models/user.py
@@ -9,6 +9,7 @@ from django.utils.translation import ugettext as _
from django.utils.html import strip_tags
from askbot import const
from askbot.utils import functions
+from askbot.models.tag import Tag
class ResponseAndMentionActivityManager(models.Manager):
def get_query_set(self):
@@ -199,9 +200,9 @@ class Activity(models.Model):
assert(user_count == 1)
return user_qs[0]
- def get_preview(self):
- if self.summary == '':
- return strip_tags(self.content_object.html)[:300]
+ def get_snippet(self, max_length = 120):
+ if self.summary == '':
+ return self.content_object.get_snippet(max_length)
else:
return self.summary
@@ -291,6 +292,8 @@ class EmailFeedSetting(models.Model):
class Meta:
#added to make account merges work properly
unique_together = ('subscriber', 'feed_type')
+ app_label = 'askbot'
+
def __str__(self):
if self.reported_at is None:
@@ -331,5 +334,26 @@ class EmailFeedSetting(models.Model):
self.reported_at = datetime.datetime.now()
self.save()
+
+class GroupMembership(models.Model):
+ """an explicit model to link users and the tags
+ that by being recorded with this relation automatically
+ become group tags
+ """
+ group = models.ForeignKey(Tag, related_name = 'user_memberships')
+ user = models.ForeignKey(User, related_name = 'group_memberships')
+
+ class Meta:
+ app_label = 'askbot'
+
+class GroupProfile(models.Model):
+ """stores group profile data"""
+ group_tag = models.OneToOneField(
+ Tag,
+ unique = True,
+ related_name = 'group_profile'
+ )
+ logo_url = models.URLField(null = True)
+
class Meta:
app_label = 'askbot'
diff --git a/askbot/skins/common/media/js/post.js b/askbot/skins/common/media/js/post.js
index 1992b635..39e6aba3 100644
--- a/askbot/skins/common/media/js/post.js
+++ b/askbot/skins/common/media/js/post.js
@@ -1542,9 +1542,11 @@ Comment.prototype.setContent = function(data){
this._comment_body.append(this._user_link);
this._comment_body.append(' (');
- this._comment_age = $('<span class="age"></span>');
- this._comment_age.html(this._data['comment_age']);
- this._comment_body.append(this._comment_age);
+ this._comment_added_at = $('<abbr class="timeago"></abbr>');
+ this._comment_added_at.html(this._data['comment_added_at']);
+ this._comment_added_at.attr('title', this._data['comment_added_at']);
+ this._comment_added_at.timeago();
+ this._comment_body.append(this._comment_added_at);
this._comment_body.append(')');
if (this._editable){
@@ -1567,8 +1569,8 @@ Comment.prototype.dispose = function(){
if (this._user_link){
this._user_link.remove();
}
- if (this._comment_age){
- this._comment_age.remove();
+ if (this._comment_added_at){
+ this._comment_added_at.remove();
}
if (this._delete_icon){
this._delete_icon.dispose();
@@ -1906,6 +1908,247 @@ QASwapper.prototype.startSwapping = function(){
}
};
+/**
+ * @constructor
+ */
+var WMD = function(){
+ WrappedElement.call(this);
+ this._markdown = undefined;
+};
+inherits(WMD, WrappedElement);
+
+WMD.prototype.setEscapeHandler = function(handler){
+ this._escape_handler = handler;
+};
+
+WMD.prototype.setSaveHandler = function(handler){
+ this._save_handler = handler;
+};
+
+WMD.prototype.createDom = function(){
+ this._element = this.makeElement('div');
+
+ var wmd_buttons = this.makeElement('div')
+ .attr('id', 'wmd-button-bar')
+ .addClass('wmd-panel');
+ this._element.append(wmd_buttons);
+
+ var editor = this.makeElement('textarea')
+ .attr('id', 'editor');
+ this._element.append(editor);
+ this._textarea = editor;
+
+ if (this._markdown){
+ editor.val(this._markdown);
+ }
+
+ var save_btn = this.makeElement('input')
+ .attr('type', 'submit')
+ .attr('value', gettext('Save'));
+ this._save_btn = save_btn;
+ this._element.append(save_btn);
+
+ var previewer = this.makeElement('div')
+ .attr('id', 'previewer')
+ .addClass('wmd-preview');
+
+ this._element.append(previewer);
+};
+
+WMD.prototype.setMarkdown = function(text){
+ this._markdown = text;
+ if (this._textarea){
+ this._textarea.val(text);
+ }
+};
+
+WMD.prototype.getMarkdown = function(){
+ return this._textarea.val();
+};
+
+WMD.prototype.start = function(){
+ Attacklab.Util.startEditor();
+ setupButtonEventHandlers(this._save_btn, this._save_handler);
+ this._textarea.keyup(makeKeyHandler(27, this._escape_handler));
+};
+
+/**
+ * @constructor
+ */
+var TagWikiEditor = function(){
+ WrappedElement.call(this);
+ this._state = 'display';//'edit' or 'display'
+ this._content_backup = '';
+};
+inherits(TagWikiEditor, WrappedElement);
+
+TagWikiEditor.prototype.backupContent = function(){
+ this._content_backup = this._content_box.contents();
+};
+
+TagWikiEditor.prototype.setContent = function(content){
+ this._content_box.empty();
+ this._content_box.append(content);
+};
+
+TagWikiEditor.prototype.restoreContent = function(){
+ var content_box = this._content_box;
+ content_box.empty();
+ $.each(this._content_backup, function(idx, element){
+ content_box.append(element);
+ });
+};
+
+TagWikiEditor.prototype.getTagId = function(){
+ return this._tag_id;
+};
+
+TagWikiEditor.prototype.startActivatingEditor = function(){
+ var editor = this._editor;
+ var me = this;
+ $.ajax({
+ type: 'GET',
+ url: askbot['urls']['load_tag_wiki_text'],
+ data: {tag_id: me.getTagId()},
+ cache: false,
+ success: function(data){
+ me.backupContent();
+ editor.setMarkdown(data);
+ me.setContent(editor.getElement());
+ editor.start();
+ }
+ });
+};
+
+TagWikiEditor.prototype.saveData = function(markdown){
+ var me = this;
+ $.ajax({
+ type: 'POST',
+ dataType: 'json',
+ url: askbot['urls']['save_tag_wiki_text'],
+ data: {tag_id: me.getTagId(), text: markdown},
+ cache: false,
+ success: function(data){
+ if (data['success']){
+ me.setContent(data['html']);
+ } else {
+ showMessage(me.getElement(), data['message']);
+ }
+ }
+ });
+};
+
+TagWikiEditor.prototype.decorate = function(element){
+ //expect <div id='group-wiki-{{id}}'><div class="content"/><a class="edit"/></div>
+ this._element = element;
+ var edit_link = element.find('.edit');
+ this._edit_link = edit_link;
+ this._content_box = element.find('.content');
+ this._tag_id = element.attr('id').split('-').pop();
+
+ var me = this;
+ var editor = new WMD();
+ editor.setEscapeHandler(function(){ me.restoreContent() });
+ editor.setSaveHandler(
+ function(){
+ me.saveData(editor.getMarkdown())
+ }
+ );
+ this._editor = editor;
+
+ setupButtonEventHandlers(edit_link, function(){ me.startActivatingEditor() });
+};
+
+var ImageChanger = function(){
+ WrappedElement.call(this);
+ this._image_element = undefined;
+};
+inherits(ImageChanger, WrappedElement);
+
+ImageChanger.prototype.setImageElement = function(image_element){
+ this._image_element = image_element;
+};
+
+ImageChanger.prototype.setSaveUrl = function(url){
+ this._save_url = url;
+};
+
+ImageChanger.prototype.setAjaxData = function(data){
+ this._ajax_data = data;
+};
+
+ImageChanger.prototype.showImage = function(image_url){
+ this._image_element.attr('src', image_url);
+};
+
+ImageChanger.prototype.saveImageUrl = function(image_url){
+ var me = this;
+ var data = this._ajax_data;
+ data['image_url'] = image_url;
+ var save_url = this._save_url;
+ $.ajax({
+ type: 'POST',
+ dataType: 'json',
+ url: save_url,
+ data: data,
+ cache: false,
+ success: function(data){
+ if (!data['success']){
+ showMessage(me.getElement(), data['message'], 'after');
+ }
+ }
+ });
+};
+
+ImageChanger.prototype.startDialog = function(){
+ //reusing the wmd's file uploader
+ var me = this;
+ Attacklab.Util.prompt(
+ "<h3>" + gettext('Enter the logo url or upload an image') + '</h3>',
+ 'http://',
+ function(image_url){
+ if (image_url){
+ me.saveImageUrl(image_url);
+ me.showImage(image_url);
+ }
+ },
+ 'image'
+ );
+};
+
+/**
+ * decorates an element that will serve as the image changer button
+ */
+ImageChanger.prototype.decorate = function(element){
+ this._element = element;
+ var me = this;
+ setupButtonEventHandlers(
+ element,
+ function(){
+ me.startDialog();
+ }
+ );
+};
+
+var UserGroupProfileEditor = function(){
+ TagWikiEditor.call(this);
+};
+inherits(UserGroupProfileEditor, TagWikiEditor);
+
+UserGroupProfileEditor.prototype.decorate = function(element){
+ UserGroupProfileEditor.superClass_.decorate.call(this, element);
+ var change_logo_btn = element.find('.change_logo');
+ this._change_logo_btn = change_logo_btn;
+
+ var logo_changer = new ImageChanger();
+ logo_changer.setImageElement(element.find('.group-logo'));
+ logo_changer.setAjaxData({
+ group_id: this.getTagId()
+ });
+ logo_changer.setSaveUrl(askbot['urls']['save_group_logo_url']);
+ logo_changer.decorate(change_logo_btn);
+};
+
$(document).ready(function() {
$('[id^="comments-for-"]').each(function(index, element){
var comments = new PostCommentsWidget();
diff --git a/askbot/skins/common/media/js/user.js b/askbot/skins/common/media/js/user.js
index 5d205560..24ca060f 100644
--- a/askbot/skins/common/media/js/user.js
+++ b/askbot/skins/common/media/js/user.js
@@ -18,7 +18,7 @@ $(document).ready(function(){
};
var submit = function(id_list, elements, action_type){
- if (action_type == 'delete' || action_type == 'mark_new' || action_type == 'mark_seen' || action_type == 'remove_flag' || action_type == 'close' || action_type == 'delete_post'){
+ if (action_type == 'delete' || action_type == 'mark_new' || action_type == 'mark_seen' || action_type == 'remove_flag' || action_type == 'delete_post'){
$.ajax({
type: 'POST',
cache: false,
@@ -26,8 +26,8 @@ $(document).ready(function(){
data: JSON.stringify({memo_list: id_list, action_type: action_type}),
url: askbot['urls']['manageInbox'],
success: function(response_data){
- if (response_data['success'] === true){
- if (action_type == 'delete' || action_type == 'remove_flag' || action_type == 'close' || action_type == 'delete_post'){
+ if (response_data['success'] == true){
+ if (action_type == 'delete' || action_type == 'remove_flag' || action_type == 'delete_post'){
elements.remove();
}
else if (action_type == 'mark_new'){
@@ -69,8 +69,11 @@ $(document).ready(function(){
}
}
if (action_type == 'remove_flag'){
- msg = ngettext('Remove all flags on this entry?',
- 'Remove all flags on these entries?', data['id_list'].length);
+ msg = ngettext(
+ 'Remove all flags and approve this entry?',
+ 'Remove all flags and approve these entries?',
+ data['id_list'].length
+ );
if (confirm(msg) === false){
return;
}
@@ -88,7 +91,7 @@ $(document).ready(function(){
setupButtonEventHandlers($('#re_mark_new'), function(){startAction('mark_new')});
setupButtonEventHandlers($('#re_dismiss'), function(){startAction('delete')});
setupButtonEventHandlers($('#re_remove_flag'), function(){startAction('remove_flag')});
- setupButtonEventHandlers($('#re_close'), function(){startAction('close')});
+ //setupButtonEventHandlers($('#re_close'), function(){startAction('close')});
setupButtonEventHandlers($('#re_delete_post'), function(){startAction('delete_post')});
setupButtonEventHandlers(
$('#sel_all'),
@@ -117,15 +120,31 @@ $(document).ready(function(){
}
);
- setupButtonEventHandlers($('.re_expand'),
- function(e){
- e.preventDefault();
- var re_snippet = $(this).find(".re_snippet:first")
- var re_content = $(this).find(".re_content:first")
- $(re_snippet).slideToggle();
- $(re_content).slideToggle();
- }
- );
+ //setupButtonEventHandlers($('.re_expand'),
+ // function(e){
+ // e.preventDefault();
+ // var re_snippet = $(this).find(".re_snippet:first")
+ // var re_content = $(this).find(".re_content:first")
+ // $(re_snippet).slideToggle();
+ // $(re_content).slideToggle();
+ // }
+ //);
+
+ $('.badge-context-toggle').each(function(idx, elem){
+ var context_list = $(elem).parent().next('ul');
+ if (context_list.children().length > 0){
+ $(elem).addClass('active');
+ var toggle_display = function(){
+ if (context_list.css('display') == 'none'){
+ $('.badge-context-list').hide();
+ context_list.show();
+ } else {
+ context_list.hide();
+ }
+ };
+ $(elem).click(toggle_display);
+ }
+ });
});
/**
@@ -204,6 +223,240 @@ FollowUser.prototype.toggleState = function(){
}
};
+/**
+ * @constructor
+ * @param {string} name
+ */
+var UserGroup = function(name){
+ WrappedElement.call(this);
+ this._name = name;
+};
+inherits(UserGroup, WrappedElement);
+
+UserGroup.prototype.getDeleteHandler = function(){
+ var group_name = this._name;
+ var me = this;
+ var groups_container = me._groups_container;
+ return function(){
+ var data = {
+ user_id: askbot['data']['viewUserId'],
+ group_name: group_name,
+ action: 'remove'
+ };
+ $.ajax({
+ type: 'POST',
+ dataType: 'json',
+ data: data,
+ cache: false,
+ url: askbot['urls']['edit_group_membership'],
+ success: function(){
+ groups_container.removeGroup(me);
+ }
+ });
+ };
+};
+
+UserGroup.prototype.getName = function(){
+ return this._name;
+};
+
+UserGroup.prototype.setGroupsContainer = function(container){
+ this._groups_container = container;
+};
+
+UserGroup.prototype.decorate = function(element){
+ this._element = element;
+ this._name = $.trim(element.html());
+ var deleter = new DeleteIcon();
+ deleter.setHandler(this.getDeleteHandler());
+ deleter.setContent('x');
+ this._element.append(deleter.getElement());
+ this._delete_icon = deleter;
+};
+
+UserGroup.prototype.createDom = function(){
+ var element = this.makeElement('li');
+ element.html(this._name + ' ');
+ this._element = element;
+ this.decorate(element);
+};
+
+UserGroup.prototype.dispose = function(){
+ this._delete_icon.dispose();
+ this._element.remove();
+};
+
+/**
+ * @constructor
+ */
+var GroupsContainer = function(){
+ WrappedElement.call(this);
+};
+inherits(GroupsContainer, WrappedElement);
+
+GroupsContainer.prototype.decorate = function(element){
+ this._element = element;
+ var groups = [];
+ var group_names = [];
+ var me = this;
+ //collect list of groups
+ $.each(element.find('li'), function(idx, li){
+ var group = new UserGroup();
+ group.setGroupsContainer(me);
+ group.decorate($(li));
+ groups.push(group);
+ group_names.push(group.getName());
+ });
+ this._groups = groups;
+ this._group_names = group_names;
+};
+
+GroupsContainer.prototype.addGroup = function(group_name){
+ if ($.inArray(group_name, this._group_names) > -1){
+ return;
+ }
+ var group = new UserGroup(group_name);
+ group.setGroupsContainer(this);
+ this._groups.push(group);
+ this._group_names.push(group_name);
+ this._element.append(group.getElement());
+};
+
+GroupsContainer.prototype.removeGroup = function(group){
+ var idx = $.inArray(group, this._groups);
+ if (idx === -1){
+ return;
+ }
+ this._groups.splice(idx, 1);
+ this._group_names.splice(idx, 1);
+ group.dispose();
+};
+
+var GroupAdderWidget = function(){
+ WrappedElement.call(this);
+ this._state = 'display';//display or edit
+};
+inherits(GroupAdderWidget, WrappedElement);
+
+/**
+ * @param {string} state
+ */
+GroupAdderWidget.prototype.setState = function(state){
+ if (state === 'display'){
+ this._element.html(gettext('add group'));
+ this._input.hide();
+ this._input.val('');
+ this._button.hide();
+ } else if (state === 'edit'){
+ this._element.html(gettext('cancel'));
+ this._input.show();
+ this._input.focus();
+ this._button.show();
+ } else {
+ return;
+ }
+ this._state = state;
+};
+
+GroupAdderWidget.prototype.getValue = function(){
+ return this._input.val();
+};
+
+GroupAdderWidget.prototype.addGroup = function(group){
+ this._groups_container.addGroup(group);
+};
+
+GroupAdderWidget.prototype.getAddGroupHandler = function(){
+ var me = this;
+ return function(){
+ var group_name = me.getValue();
+ var data = {
+ group_name: group_name,
+ user_id: askbot['data']['viewUserId'],
+ action: 'add'
+ };
+ $.ajax({
+ type: 'POST',
+ dataType: 'json',
+ data: data,
+ cache: false,
+ url: askbot['urls']['edit_group_membership'],
+ success: function(data){
+ if (data['success'] == true){
+ me.addGroup(group_name);
+ me.setState('display');
+ } else {
+ var message = data['message'];
+ showMessage(me.getElement(), message, 'after');
+ }
+ }
+ });
+ };
+};
+
+GroupAdderWidget.prototype.setGroupsContainer = function(container){
+ this._groups_container = container;
+};
+
+GroupAdderWidget.prototype.toggleState = function(){
+ if (this._state === 'display'){
+ this.setState('edit');
+ } else if (this._state === 'edit'){
+ this.setState('display');
+ }
+};
+
+GroupAdderWidget.prototype.decorate = function(element){
+ this._element = element;
+ var input = this.makeElement('input');
+ this._input = input;
+
+ var groupsAc = new AutoCompleter({
+ url: askbot['urls']['get_groups_list'],
+ preloadData: true,
+ minChars: 1,
+ useCache: true,
+ matchInside: false,
+ maxCacheLength: 100,
+ delay: 10
+ });
+ groupsAc.decorate(input);
+
+ var button = this.makeElement('button');
+ button.html(gettext('add'));
+ this._button = button;
+ element.before(input);
+ input.after(button);
+ this.setState('display');
+ setupButtonEventHandlers(button, this.getAddGroupHandler());
+ var me = this;
+ setupButtonEventHandlers(
+ element,
+ function(){ me.toggleState() }
+ );
+};
+
+/**
+ * @constructor
+ * allows editing user groups
+ */
+var UserGroupsEditor = function(){
+ WrappedElement.call(this);
+};
+inherits(UserGroupsEditor, WrappedElement);
+
+UserGroupsEditor.prototype.decorate = function(element){
+ this._element = element;
+ var add_link = element.find('#add-group');
+ var adder = new GroupAdderWidget();
+ adder.decorate(add_link);
+
+ var groups_container = new GroupsContainer();
+ groups_container.decorate(element.find('ul'));
+ adder.setGroupsContainer(groups_container);
+ //todo - add group deleters
+};
+
(function(){
var fbtn = $('.follow-toggle');
if (fbtn.length === 1){
@@ -211,5 +464,10 @@ FollowUser.prototype.toggleState = function(){
follow_user.decorate(fbtn);
follow_user.setUserName(askbot['data']['viewUserName']);
}
+ if (askbot['data']['userIsAdminOrMod']){
+ var group_editor = new UserGroupsEditor();
+ group_editor.decorate($('#user-groups'));
+ } else {
+ $('#add-group').remove();
+ }
})();
-
diff --git a/askbot/skins/common/media/js/utils.js b/askbot/skins/common/media/js/utils.js
index 9e02b5d4..42a7e3be 100644
--- a/askbot/skins/common/media/js/utils.js
+++ b/askbot/skins/common/media/js/utils.js
@@ -240,6 +240,9 @@ WrappedElement.prototype.setElement = function(element){
WrappedElement.prototype.createDom = function(){
this._element = $('<div></div>');
};
+WrappedElement.prototype.decorate = function(element){
+ this._element = element;
+};
WrappedElement.prototype.getElement = function(){
if (this._element === null){
this.createDom();
@@ -308,6 +311,7 @@ EditLink.prototype.decorate = function(element){
var DeleteIcon = function(title){
SimpleControl.call(this);
this._title = title;
+ this._content = null;
};
inherits(DeleteIcon, SimpleControl);
@@ -327,8 +331,20 @@ DeleteIcon.prototype.setHandlerInternal = function(){
DeleteIcon.prototype.createDom = function(){
this._element = this.makeElement('span');
this.decorate(this._element);
+ if (this._content !== null){
+ this.setContent(this._content);
+ }
};
+DeleteIcon.prototype.setContent = function(content){
+ if (this._element === null){
+ this._content = content;
+ } else {
+ this._content = content;
+ this._element.html(content);
+ }
+}
+
var Tag = function(){
SimpleControl.call(this);
this._deletable = false;
@@ -490,3 +506,205 @@ if(!this.JSON){this.JSON={}}(function(){function f(n){return n<10?"0"+n:n}if(typ
//our custom autocompleter
var AutoCompleter=function(a){var b={autocompleteMultiple:true,multipleSeparator:" ",inputClass:"acInput",loadingClass:"acLoading",resultsClass:"acResults",selectClass:"acSelect",queryParamName:"q",limitParamName:"limit",extraParams:{},lineSeparator:"\n",cellSeparator:"|",minChars:2,maxItemsToShow:10,delay:400,useCache:true,maxCacheLength:10,matchSubset:true,matchCase:false,matchInside:true,mustMatch:false,preloadData:false,selectFirst:false,stopCharRegex:/\s+/,selectOnly:false,formatItem:null,onItemSelect:false,autoFill:false,filterResults:true,sortResults:true,sortFunction:false,onNoMatch:false};this.options=$.extend({},b,a);this.cacheData_={};this.cacheLength_=0;this.selectClass_="jquery-autocomplete-selected-item";this.keyTimeout_=null;this.lastKeyPressed_=null;this.lastProcessedValue_=null;this.lastSelectedValue_=null;this.active_=false;this.finishOnBlur_=true;this.options.minChars=parseInt(this.options.minChars,10);if(isNaN(this.options.minChars)||this.options.minChars<1){this.options.minChars=2}this.options.maxItemsToShow=parseInt(this.options.maxItemsToShow,10);if(isNaN(this.options.maxItemsToShow)||this.options.maxItemsToShow<1){this.options.maxItemsToShow=10}this.options.maxCacheLength=parseInt(this.options.maxCacheLength,10);if(isNaN(this.options.maxCacheLength)||this.options.maxCacheLength<1){this.options.maxCacheLength=10}if(this.options.preloadData===true){this.fetchRemoteData("",function(){})}};inherits(AutoCompleter,WrappedElement);AutoCompleter.prototype.decorate=function(a){this._element=a;this._element.attr("autocomplete","off");this._results=$("<div></div>").hide();if(this.options.resultsClass){this._results.addClass(this.options.resultsClass)}this._results.css({position:"absolute"});$("body").append(this._results);this.setEventHandlers()};AutoCompleter.prototype.setEventHandlers=function(){var a=this;a._element.keydown(function(b){a.lastKeyPressed_=b.keyCode;switch(a.lastKeyPressed_){case 38:b.preventDefault();if(a.active_){a.focusPrev()}else{a.activate()}return false;break;case 40:b.preventDefault();if(a.active_){a.focusNext()}else{a.activate()}return false;break;case 9:case 13:if(a.active_){b.preventDefault();a.selectCurrent();return false}break;case 27:if(a.active_){b.preventDefault();a.finish();return false}break;default:a.activate()}});a._element.blur(function(){if(a.finishOnBlur_){setTimeout(function(){a.finish()},200)}})};AutoCompleter.prototype.position=function(){var a=this._element.offset();this._results.css({top:a.top+this._element.outerHeight(),left:a.left})};AutoCompleter.prototype.cacheRead=function(d){var f,c,b,a,e;if(this.options.useCache){d=String(d);f=d.length;if(this.options.matchSubset){c=1}else{c=f}while(c<=f){if(this.options.matchInside){a=f-c}else{a=0}e=0;while(e<=a){b=d.substr(0,c);if(this.cacheData_[b]!==undefined){return this.cacheData_[b]}e++}c++}}return false};AutoCompleter.prototype.cacheWrite=function(a,b){if(this.options.useCache){if(this.cacheLength_>=this.options.maxCacheLength){this.cacheFlush()}a=String(a);if(this.cacheData_[a]!==undefined){this.cacheLength_++}return this.cacheData_[a]=b}return false};AutoCompleter.prototype.cacheFlush=function(){this.cacheData_={};this.cacheLength_=0};AutoCompleter.prototype.callHook=function(c,b){var a=this.options[c];if(a&&$.isFunction(a)){return a(b,this)}return false};AutoCompleter.prototype.activate=function(){var b=this;var a=function(){b.activateNow()};var c=parseInt(this.options.delay,10);if(isNaN(c)||c<=0){c=250}if(this.keyTimeout_){clearTimeout(this.keyTimeout_)}this.keyTimeout_=setTimeout(a,c)};AutoCompleter.prototype.activateNow=function(){var a=this.getValue();if(a!==this.lastProcessedValue_&&a!==this.lastSelectedValue_){if(a.length>=this.options.minChars){this.active_=true;this.lastProcessedValue_=a;this.fetchData(a)}}};AutoCompleter.prototype.fetchData=function(b){if(this.options.data){this.filterAndShowResults(this.options.data,b)}else{var a=this;this.fetchRemoteData(b,function(c){a.filterAndShowResults(c,b)})}};AutoCompleter.prototype.fetchRemoteData=function(c,e){var d=this.cacheRead(c);if(d){e(d)}else{var a=this;if(this._element){this._element.addClass(this.options.loadingClass)}var b=function(g){var f=false;if(g!==false){f=a.parseRemoteData(g);a.options.data=f;a.cacheWrite(c,f)}if(a._element){a._element.removeClass(a.options.loadingClass)}e(f)};$.ajax({url:this.makeUrl(c),success:b,error:function(){b(false)}})}};AutoCompleter.prototype.setOption=function(a,b){this.options[a]=b};AutoCompleter.prototype.setExtraParam=function(b,c){var a=$.trim(String(b));if(a){if(!this.options.extraParams){this.options.extraParams={}}if(this.options.extraParams[a]!==c){this.options.extraParams[a]=c;this.cacheFlush()}}};AutoCompleter.prototype.makeUrl=function(e){var a=this;var b=this.options.url;var d=$.extend({},this.options.extraParams);if(this.options.queryParamName===false){b+=encodeURIComponent(e)}else{d[this.options.queryParamName]=e}if(this.options.limitParamName&&this.options.maxItemsToShow){d[this.options.limitParamName]=this.options.maxItemsToShow}var c=[];$.each(d,function(f,g){c.push(a.makeUrlParam(f,g))});if(c.length){b+=b.indexOf("?")==-1?"?":"&";b+=c.join("&")}return b};AutoCompleter.prototype.makeUrlParam=function(a,b){return String(a)+"="+encodeURIComponent(b)};AutoCompleter.prototype.splitText=function(a){return String(a).replace(/(\r\n|\r|\n)/g,"\n").split(this.options.lineSeparator)};AutoCompleter.prototype.parseRemoteData=function(c){var h,b,f,d,g;var e=[];var b=this.splitText(c);for(f=0;f<b.length;f++){var a=b[f].split(this.options.cellSeparator);g=[];for(d=0;d<a.length;d++){g.push(unescape(a[d]))}h=g.shift();e.push({value:unescape(h),data:g})}return e};AutoCompleter.prototype.filterAndShowResults=function(a,b){this.showResults(this.filterResults(a,b),b)};AutoCompleter.prototype.filterResults=function(d,b){var f=[];var l,c,e,m,j,a;var k,h,g;for(e=0;e<d.length;e++){m=d[e];j=typeof m;if(j==="string"){l=m;c={}}else{if($.isArray(m)){l=m[0];c=m.slice(1)}else{if(j==="object"){l=m.value;c=m.data}}}l=String(l);if(l>""){if(typeof c!=="object"){c={}}if(this.options.filterResults){h=String(b);g=String(l);if(!this.options.matchCase){h=h.toLowerCase();g=g.toLowerCase()}a=g.indexOf(h);if(this.options.matchInside){a=a>-1}else{a=a===0}}else{a=true}if(a){f.push({value:l,data:c})}}}if(this.options.sortResults){f=this.sortResults(f,b)}if(this.options.maxItemsToShow>0&&this.options.maxItemsToShow<f.length){f.length=this.options.maxItemsToShow}return f};AutoCompleter.prototype.sortResults=function(c,d){var b=this;var a=this.options.sortFunction;if(!$.isFunction(a)){a=function(g,e,h){return b.sortValueAlpha(g,e,h)}}c.sort(function(f,e){return a(f,e,d)});return c};AutoCompleter.prototype.sortValueAlpha=function(d,c,e){d=String(d.value);c=String(c.value);if(!this.options.matchCase){d=d.toLowerCase();c=c.toLowerCase()}if(d>c){return 1}if(d<c){return -1}return 0};AutoCompleter.prototype.showResults=function(e,b){var k=this;var g=$("<ul></ul>");var f,l,j,a,h=false,d=false;var c=e.length;for(f=0;f<c;f++){l=e[f];j=$("<li>"+this.showResult(l.value,l.data)+"</li>");j.data("value",l.value);j.data("data",l.data);j.click(function(){var i=$(this);k.selectItem(i)}).mousedown(function(){k.finishOnBlur_=false}).mouseup(function(){k.finishOnBlur_=true});g.append(j);if(h===false){h=String(l.value);d=j;j.addClass(this.options.firstItemClass)}if(f==c-1){j.addClass(this.options.lastItemClass)}}this.position();this._results.html(g).show();a=this._results.outerWidth()-this._results.width();this._results.width(this._element.outerWidth()-a);$("li",this._results).hover(function(){k.focusItem(this)},function(){});if(this.autoFill(h,b)){this.focusItem(d)}};AutoCompleter.prototype.showResult=function(b,a){if($.isFunction(this.options.showResult)){return this.options.showResult(b,a)}else{return b}};AutoCompleter.prototype.autoFill=function(e,c){var b,a,d,f;if(this.options.autoFill&&this.lastKeyPressed_!=8){b=String(e).toLowerCase();a=String(c).toLowerCase();d=e.length;f=c.length;if(b.substr(0,f)===a){this._element.val(e);this.selectRange(f,d);return true}}return false};AutoCompleter.prototype.focusNext=function(){this.focusMove(+1)};AutoCompleter.prototype.focusPrev=function(){this.focusMove(-1)};AutoCompleter.prototype.focusMove=function(a){var b,c=$("li",this._results);a=parseInt(a,10);for(var b=0;b<c.length;b++){if($(c[b]).hasClass(this.selectClass_)){this.focusItem(b+a);return}}this.focusItem(0)};AutoCompleter.prototype.focusItem=function(b){var a,c=$("li",this._results);if(c.length){c.removeClass(this.selectClass_).removeClass(this.options.selectClass);if(typeof b==="number"){b=parseInt(b,10);if(b<0){b=0}else{if(b>=c.length){b=c.length-1}}a=$(c[b])}else{a=$(b)}if(a){a.addClass(this.selectClass_).addClass(this.options.selectClass)}}};AutoCompleter.prototype.selectCurrent=function(){var a=$("li."+this.selectClass_,this._results);if(a.length==1){this.selectItem(a)}else{this.finish()}};AutoCompleter.prototype.selectItem=function(d){var c=d.data("value");var b=d.data("data");var a=this.displayValue(c,b);this.lastProcessedValue_=a;this.lastSelectedValue_=a;this.setValue(a);this.setCaret(a.length);this.callHook("onItemSelect",{value:c,data:b});this.finish()};AutoCompleter.prototype.isContentChar=function(a){if(a.match(this.options.stopCharRegex)){return false}else{if(a===this.options.multipleSeparator){return false}else{return true}}};AutoCompleter.prototype.getValue=function(){var c=this._element.getSelection();var d=this._element.val();var f=c.start;var e=f;for(cpos=f;cpos>=0;cpos=cpos-1){if(cpos===d.length){continue}var b=d.charAt(cpos);if(!this.isContentChar(b)){break}e=cpos}var a=f;for(cpos=f;cpos<d.length;cpos=cpos+1){if(cpos===0){continue}var b=d.charAt(cpos);if(!this.isContentChar(b)){break}a=cpos}this._selection_start=e;this._selection_end=a;return d.substring(e,a)};AutoCompleter.prototype.setValue=function(b){var a=this._element.val().substring(0,this._selection_start);var c=this._element.val().substring(this._selection_end+1);this._element.val(a+b+c)};AutoCompleter.prototype.displayValue=function(b,a){if($.isFunction(this.options.displayValue)){return this.options.displayValue(b,a)}else{return b}};AutoCompleter.prototype.finish=function(){if(this.keyTimeout_){clearTimeout(this.keyTimeout_)}if(this._element.val()!==this.lastSelectedValue_){if(this.options.mustMatch){this._element.val("")}this.callHook("onNoMatch")}this._results.hide();this.lastKeyPressed_=null;this.lastProcessedValue_=null;if(this.active_){this.callHook("onFinish")}this.active_=false};AutoCompleter.prototype.selectRange=function(d,a){var c=this._element.get(0);if(c.setSelectionRange){c.focus();c.setSelectionRange(d,a)}else{if(this.createTextRange){var b=this.createTextRange();b.collapse(true);b.moveEnd("character",a);b.moveStart("character",d);b.select()}}};AutoCompleter.prototype.setCaret=function(a){this.selectRange(a,a)};
(function($){function isRGBACapable(){var $script=$("script:first"),color=$script.css("color"),result=false;if(/^rgba/.test(color)){result=true}else{try{result=(color!=$script.css("color","rgba(0, 0, 0, 0.5)").css("color"));$script.css("color",color)}catch(e){}}return result}$.extend(true,$,{support:{rgba:isRGBACapable()}});var properties=["color","backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","outlineColor"];$.each(properties,function(i,property){$.fx.step[property]=function(fx){if(!fx.init){fx.begin=parseColor($(fx.elem).css(property));fx.end=parseColor(fx.end);fx.init=true}fx.elem.style[property]=calculateColor(fx.begin,fx.end,fx.pos)}});$.fx.step.borderColor=function(fx){if(!fx.init){fx.end=parseColor(fx.end)}var borders=properties.slice(2,6);$.each(borders,function(i,property){if(!fx.init){fx[property]={begin:parseColor($(fx.elem).css(property))}}fx.elem.style[property]=calculateColor(fx[property].begin,fx.end,fx.pos)});fx.init=true};function calculateColor(begin,end,pos){var color="rgb"+($.support.rgba?"a":"")+"("+parseInt((begin[0]+pos*(end[0]-begin[0])),10)+","+parseInt((begin[1]+pos*(end[1]-begin[1])),10)+","+parseInt((begin[2]+pos*(end[2]-begin[2])),10);if($.support.rgba){color+=","+(begin&&end?parseFloat(begin[3]+pos*(end[3]-begin[3])):1)}color+=")";return color}function parseColor(color){var match,triplet;if(match=/#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/.exec(color)){triplet=[parseInt(match[1],16),parseInt(match[2],16),parseInt(match[3],16),1]}else{if(match=/#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])/.exec(color)){triplet=[parseInt(match[1],16)*17,parseInt(match[2],16)*17,parseInt(match[3],16)*17,1]}else{if(match=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)){triplet=[parseInt(match[1]),parseInt(match[2]),parseInt(match[3]),1]}else{if(match=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9\.]*)\s*\)/.exec(color)){triplet=[parseInt(match[1],10),parseInt(match[2],10),parseInt(match[3],10),parseFloat(match[4])]}else{if(color=="transparent"){triplet=[0,0,0,0]}}}}}return triplet}})(jQuery);
+
+/**
+ * Timeago is a jQuery plugin that makes it easy to support automatically
+ * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
+ *
+ * @name timeago
+ * @version 0.11.1
+ * @requires jQuery v1.2.3+
+ * @author Ryan McGeary
+ * @license MIT License - http://www.opensource.org/licenses/mit-license.php
+ *
+ * For usage and examples, visit:
+ * http://timeago.yarp.com/
+ *
+ * Copyright (c) 2008-2011, Ryan McGeary (ryanonjavascript -[at]- mcgeary [*dot*] org)
+ */
+(function($) {
+ $.timeago = function(timestamp) {
+ if (timestamp instanceof Date) {
+ return inWords(timestamp);
+ } else if (typeof timestamp === "string") {
+ return inWords($.timeago.parse(timestamp));
+ } else {
+ return inWords($.timeago.datetime(timestamp));
+ }
+ };
+ var $t = $.timeago;
+
+ $.extend($.timeago, {
+ settings: {
+ refreshMillis: 60000,
+ allowFuture: false,
+ strings: {
+ prefixAgo: null,
+ prefixFromNow: null,
+ suffixAgo: gettext("ago"),
+ suffixFromNow: gettext("from now"),
+ seconds: gettext("just now"),
+ minute: gettext("about a minute"),
+ minutes: gettext("%d minutes"),
+ hour: gettext("about an hour"),
+ hours: gettext("%d hours"),
+ day: gettext("yesterday"),
+ days: gettext("%d days"),
+ month: gettext("about a month"),
+ months: gettext("%d months"),
+ year: gettext("about a year"),
+ years: gettext("%d years"),
+ wordSeparator: " ",
+ numbers: []
+ }
+ },
+ inWords: function(distanceMillis) {
+ var $l = this.settings.strings;
+ var prefix = $l.prefixAgo;
+ var suffix = $l.suffixAgo;
+ if (this.settings.allowFuture) {
+ if (distanceMillis < 0) {
+ prefix = $l.prefixFromNow;
+ suffix = $l.suffixFromNow;
+ }
+ }
+
+ var seconds = Math.abs(distanceMillis) / 1000;
+ var minutes = seconds / 60;
+ var hours = minutes / 60;
+ var days = hours / 24;
+ var years = days / 365;
+
+ function substitute(stringOrFunction, number) {
+ var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
+ var value = ($l.numbers && $l.numbers[number]) || number;
+ return string.replace(/%d/i, value);
+ }
+
+ var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
+ seconds < 90 && substitute($l.minute, 1) ||
+ minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
+ minutes < 90 && substitute($l.hour, 1) ||
+ hours < 24 && substitute($l.hours, Math.round(hours)) ||
+ hours < 42 && substitute($l.day, 1) ||
+ days < 30 && substitute($l.days, Math.round(days)) ||
+ days < 45 && substitute($l.month, 1) ||
+ days < 365 && substitute($l.months, Math.round(days / 30)) ||
+ years < 1.5 && substitute($l.year, 1) ||
+ substitute($l.years, Math.round(years));
+
+ var separator = $l.wordSeparator === undefined ? " " : $l.wordSeparator;
+ return $.trim([prefix, words, suffix].join(separator));
+ },
+ parse: function(iso8601) {
+ var s = $.trim(iso8601);
+ s = s.replace(/\.\d\d\d+/,""); // remove milliseconds
+ s = s.replace(/-/,"/").replace(/-/,"/");
+ s = s.replace(/T/," ").replace(/Z/," UTC");
+ s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
+ return new Date(s);
+ },
+ datetime: function(elem) {
+ // jQuery's `is()` doesn't play well with HTML5 in IE
+ var isTime = $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
+ var iso8601 = isTime ? $(elem).attr("datetime") : $(elem).attr("title");
+ return $t.parse(iso8601);
+ }
+ });
+
+ $.fn.timeago = function() {
+ var self = this;
+ self.each(refresh);
+
+ var $s = $t.settings;
+ if ($s.refreshMillis > 0) {
+ setInterval(function() { self.each(refresh); }, $s.refreshMillis);
+ }
+ return self;
+ };
+
+ function refresh() {
+ var data = prepareData(this);
+ if (!isNaN(data.datetime)) {
+ $(this).text(inWords(data.datetime));
+ }
+ return this;
+ }
+
+ function prepareData(element) {
+ element = $(element);
+ if (!element.data("timeago")) {
+ element.data("timeago", { datetime: $t.datetime(element) });
+ var text = $.trim(element.text());
+ if (text.length > 0) {
+ element.attr("title", text);
+ }
+ }
+ return element.data("timeago");
+ }
+
+ function inWords(date) {
+ var distanceMillis = distance(date);
+ var seconds = Math.abs(distanceMillis) / 1000;
+ var minutes = seconds / 60;
+ var hours = minutes / 60;
+ var days = hours / 24;
+ var years = days / 365;
+ var months = [
+ gettext('Jan'),
+ gettext('Feb'),
+ gettext('Mar'),
+ gettext('Apr'),
+ gettext('May'),
+ gettext('Jun'),
+ gettext('Jul'),
+ gettext('Aug'),
+ gettext('Sep'),
+ gettext('Oct'),
+ gettext('Nov'),
+ gettext('Dec')
+ ];
+ //todo: rewrite this in javascript
+ if (days > 2){
+ var month_date = months[date.getMonth()] + ' ' + date.getDate()
+ if (years == 0){
+ //how to do this in js???
+ return month_date;
+ } else {
+ return month_date + ' ' + "'" + date.getYear() % 20;
+ }
+ } else if (days == 2) {
+ return gettext('2 days ago')
+ } else if (days == 1) {
+ return gettext('yesterday')
+ } else if (minutes >= 60) {
+ return interpolate(
+ ngettext(
+ '%s hour ago',
+ '%s hours ago',
+ hours
+ ),
+ [Math.floor(hours),]
+ )
+ } else if (seconds > 90){
+ return interpolate(
+ ngettext(
+ '%s min ago',
+ '%s mins ago',
+ minutes
+ ),
+ [Math.floor(minutes),]
+ )
+ } else {
+ return gettext('just now')
+ }
+ }
+
+ function distance(date) {
+ return (new Date() - date);
+ }
+
+ // fix for IE6 suckage
+ document.createElement("abbr");
+ document.createElement("time");
+}(jQuery));
diff --git a/askbot/skins/common/media/js/wmd/wmd.js b/askbot/skins/common/media/js/wmd/wmd.js
index 1d524361..9f941855 100644
--- a/askbot/skins/common/media/js/wmd/wmd.js
+++ b/askbot/skins/common/media/js/wmd/wmd.js
@@ -346,6 +346,9 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){
style.marginLeft = style.marginRight = "auto";
form.appendChild(input);
+ //EF. fucus at the end of the input box
+ //putCursorAtEnd($(input));
+
// The upload file input
if(dialogType == 'image' || dialogType == 'file'){
var upload_container = $('<div></div>');
diff --git a/askbot/skins/common/templates/authopenid/signin.html b/askbot/skins/common/templates/authopenid/signin.html
index e849db20..f9c0cfea 100644
--- a/askbot/skins/common/templates/authopenid/signin.html
+++ b/askbot/skins/common/templates/authopenid/signin.html
@@ -1,5 +1,6 @@
{% extends "two_column_body.html" %}
{% import "authopenid/authopenid_macros.html" as login_macros %}
+{% from "macros.html" import timeago %}
<!-- signin.html -->
{% block title %}{% spaceless %}{% trans %}User login{% endtrans %}{% endspaceless %}{% endblock %}
{% block forestyle %}
@@ -169,7 +170,7 @@
</td>
<td>
{% if login_method.last_used_timestamp %}
- {{login_method.last_used_timestamp|diff_date}}
+ {{ timeago(login_method.last_used_timestamp) }}
{% endif %}
</td>
<td>
diff --git a/askbot/skins/common/templates/question/answer_author_info.html b/askbot/skins/common/templates/question/answer_author_info.html
index f17cb62c..1c729b51 100644
--- a/askbot/skins/common/templates/question/answer_author_info.html
+++ b/askbot/skins/common/templates/question/answer_author_info.html
@@ -1,6 +1,8 @@
{{
macros.post_last_updater_and_creator_info(
answer,
- settings.MIN_REP_TO_EDIT_WIKI
+ settings.MIN_REP_TO_EDIT_WIKI,
+ karma_mode = settings.KARMA_MODE,
+ badges_mode = settings.BADGES_MODE
)
}}
diff --git a/askbot/skins/common/templates/question/question_author_info.html b/askbot/skins/common/templates/question/question_author_info.html
index e43f8931..c25b7d84 100644
--- a/askbot/skins/common/templates/question/question_author_info.html
+++ b/askbot/skins/common/templates/question/question_author_info.html
@@ -1,6 +1,8 @@
{{
macros.post_last_updater_and_creator_info(
question,
- settings.MIN_REP_TO_EDIT_WIKI
+ settings.MIN_REP_TO_EDIT_WIKI,
+ karma_mode = settings.KARMA_MODE,
+ badges_mode = settings.BADGES_MODE
)
}}
diff --git a/askbot/skins/default/media/style/lib_style.less b/askbot/skins/default/media/style/lib_style.less
index 941c83ff..795733e5 100644
--- a/askbot/skins/default/media/style/lib_style.less
+++ b/askbot/skins/default/media/style/lib_style.less
@@ -14,7 +14,7 @@
@body-font:Arial; /* "Trebuchet MS", sans-serif;*/
@sort-font:Georgia, serif;
-@main-font:'Yanone Kaffeesatz', sans-serif;
+@main-font:'Yanone Kaffeesatz', Arial, sans-serif;
@secondary-font:Arial;
/* Receive exactly positions for background Sprite */
diff --git a/askbot/skins/default/media/style/style.less b/askbot/skins/default/media/style/style.less
index e63ff373..5b24a15c 100644
--- a/askbot/skins/default/media/style/style.less
+++ b/askbot/skins/default/media/style/style.less
@@ -312,6 +312,10 @@ body.user-messages {
.sprites(-125px,-5px)
}
+ #navGroups{
+ .sprites(-125px,-5px)
+ }
+
#navBadges{
.sprites(-210px,-5px)
}
@@ -768,8 +772,7 @@ body.anon {
.tabsA .label, .tabsC .label {
float: left;
color: #646464;
- margin-top:4px;
- margin-right:5px;
+ margin:4px 5px 0px 8px;
}
.main-page .tabsA .label {
@@ -896,7 +899,7 @@ ul#searchTags {
}
- .userinfo .relativetime, span.anonymous
+ .userinfo .timeago, span.anonymous
{
font-size: 11px;
clear:both;
@@ -2242,14 +2245,15 @@ ul#related-tags li {
/* People page */
-.tabBar-user{
+/*.users-page .tabBar{
width:375px;
-}
+}*/
.user {
padding: 5px;
line-height: 140%;
width: 166px;
+ height: 32px;
border:#eee 1px solid;
margin-bottom:5px;
.rounded-corners(3px);
@@ -2801,7 +2805,7 @@ span.form-error {
margin: 0px;
}
-.relativetime {
+.timeago {
font-weight: bold;
text-decoration: none;
}
@@ -3383,3 +3387,45 @@ body.anon.lang-es {
}
}
}
+
+/* user groups */
+#user-groups ul {
+ margin-bottom: 0px;
+}
+#user-groups .delete-icon {
+ float: none;
+ display: inline;
+ color: #525252;
+ padding: 0 3px 0 3px;
+ background: #ccc;
+ border-radius: 4px;
+ line-height:inherit;
+ -moz-border-radius: 4px;
+ -khtml-border-radius: 4px;
+ -webkit-border-radius: 4px;
+}
+#user-groups .delete-icon:hover {
+ color: white;
+ background: #b32f2f;
+}
+
+.users-page {
+ #editor {
+ border: 2px #CCE6EC solid;
+ padding: 0px;
+ }
+ .wmd-prompt-dialog {
+ background: #ccc;
+ }
+}
+
+.group-wiki {
+ .content {
+ float: left;
+ margin-bottom: -12px;
+ }
+ .group-logo {
+ float: left;
+ margin: 0 5px 3px 0;
+ }
+}
diff --git a/askbot/skins/default/templates/groups.html b/askbot/skins/default/templates/groups.html
new file mode 100644
index 00000000..9ca49c85
--- /dev/null
+++ b/askbot/skins/default/templates/groups.html
@@ -0,0 +1,22 @@
+{% extends 'one_column_body.html' %}
+{% block title %}{% trans %}Groups{% endtrans %}{% endblock %}
+{% block content %}
+ <h1>{% trans %}Groups{% endtrans %}</h1>
+ {% if can_edit %}
+ <p id="group-add-tip">
+ {% trans %}Tip: to create a new group - please go to some user profile and add the new group there. That user will be the first member of the group{% endtrans %}
+ </p>
+ {% endif %}
+ <ul id="groups-list">
+ {% for group in groups %}
+ <li>
+ <a href="{% url users_by_group group.id, group.name|slugify %}">{{ group.name|escape }}</a>
+ <div id="group-{{group.id}}-description">
+ {% if group.tag_wiki %}
+ {{ group.tag_wiki.html }}
+ {% endif %}
+ </div>
+ </li>
+ {% endfor %}
+ </ul>
+{% endblock %}
diff --git a/askbot/skins/default/templates/instant_notification.html b/askbot/skins/default/templates/instant_notification.html
index 92799a96..cd6e5427 100644
--- a/askbot/skins/default/templates/instant_notification.html
+++ b/askbot/skins/default/templates/instant_notification.html
@@ -1,41 +1,6 @@
-{% trans %}<p>Dear {{receiving_user_name}},</p>{% endtrans %}
- {% if update_type == 'question_comment' %}
+{{ reply_separator }}
+<div>{{ content_preview }}</div>
{% trans %}
-<p>{{update_author_name}} left a <a href="{{post_url}}">new comment</a>:</p>
-{% endtrans %}
- {% endif %}
- {% if update_type == 'answer_comment' %}
-{% trans %}
-<p>{{update_author_name}} left a <a href="{{post_url}}">new comment</a></p>
-{% endtrans %}
- {% endif %}
- {% if update_type == 'new_answer' %}
-{% trans %}
-<p>{{update_author_name}} answered a question
-<a href="{{post_url}}">{{origin_post_title}}</a></p>
-{% endtrans %}
- {% endif %}
- {% if update_type == 'new_question' %}
-{% trans %}
-<p>{{update_author_name}} posted a new question
-<a href="{{post_url}}">{{origin_post_title}}</a></p>
-{% endtrans %}
- {% endif %}
- {%if update_type == 'answer_update' %}
-{% trans %}
-<p>{{update_author_name}} updated an answer to the question
-<a href="{{post_url}}">{{origin_post_title}}</a></p>
-{% endtrans %}
- {% endif %}
- {% if update_type == 'question_update' %}
-{% trans %}
-<p>{{update_author_name}} updated a question
-<a href="{{post_url}}">{{origin_post_title}}</a></p>
-{% endtrans %}
- {% endif %}
-<p></p>
-{% trans %}
-<div>{{content_preview}}</div>
<p>Please note - you can easily <a href="{{user_subscriptions_url}}">change</a>
how often you receive these notifications or unsubscribe. Thank you for your interest in our forum!</p>
{% endtrans %}
diff --git a/askbot/skins/default/templates/instant_notification_reply_by_email.html b/askbot/skins/default/templates/instant_notification_reply_by_email.html
deleted file mode 100644
index ffb43110..00000000
--- a/askbot/skins/default/templates/instant_notification_reply_by_email.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-{% if can_reply %}
-{% trans %}
-{# Don't change the following line in the template. #}
-======= Reply above this line. ====-=-=
-{% endtrans %}
-{% else %}
-{% trans %}
-You can post an answer or a comment by replying to email notifications. To do that
-you need {{reply_by_email_karma_threshold}} karma, you have {{receiving_user_karma}} karma.
-{% endtrans %}
-{% endif %}
-
-{% include 'instant_notification.html' %} \ No newline at end of file
diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html
index 1c6b925c..39562ef5 100644
--- a/askbot/skins/default/templates/macros.html
+++ b/askbot/skins/default/templates/macros.html
@@ -23,6 +23,29 @@
</div>
{%- endmacro -%}
+{%- macro inbox_post_snippet(response, inbox_section) -%}
+<div id="re_{{response.id}}" class="re{% if response.is_new %} new highlight{% else %} seen{% endif %}">
+ <input type="checkbox" />
+ <div class="face">
+ {{ gravatar(response.user, 48) }}
+ </div>
+ <a style="font-size:12px" href="{{ response.user.get_absolute_url() }}">{{ response.user.username }}</a>
+ <a style="text-decoration:none;" href="{{ response.response_url }}">
+ {{ response.response_type }}
+ ({{ timeago(response.timestamp) }}):<br/>
+ {% if inbox_section != 'flags' %}
+ {{ response.response_snippet }}
+ {% endif %}
+ </a>
+ {% if inbox_section == 'flags' %}
+ <a class="re_expand" href="{{ response.response_url }}">
+ <!--div class="re_snippet">{{ response.response_snippet }}</div-->
+ <div class="re_content">{{ response.response_content }}</div>
+ </a>
+ {% endif %}
+</div>
+{%- endmacro -%}
+
{%- macro post_vote_buttons(post = None) -%}
<div id="{{post.post_type}}-img-upvote-{{ post.id }}"
class="{{post.post_type}}-img-upvote post-vote">
@@ -41,24 +64,37 @@
</script>
{%- endmacro -%}
-{%- macro post_contributor_avatar_and_credentials(post, user) -%}
+{%- macro post_contributor_avatar_and_credentials(post, user, karma_mode = None, badges_mode = None) -%}
{% if post.is_anonymous %}
<img alt="{% trans %}anonymous user{% endtrans %}" src="{{ '/images/anon.png'|media }} " class="gravatar" width="32" height="32" />
<p>{{ user.get_anonymous_name() }}</p>
{% else %}
{{ gravatar(user, 32) }}
{{ user.get_profile_link()}}{{ user_country_flag(user) }}<br/>
- {{ user_score_and_badge_summary(user) }}<br/>
+ {{ user_score_and_badge_summary(user, karma_mode = karma_mode, badges_mode = badges_mode) }}<br/>
{{ user_website_link(user) }}
{% endif %}
{%- endmacro -%}
-{%- macro post_last_updater_and_creator_info(post, min_rep_to_edit_wiki) -%}
- {{ post_contributor_info(post, "original_author", post.wiki, min_rep_to_edit_wiki) }}
- {{ post_contributor_info(post, "last_updater", post.wiki, min_rep_to_edit_wiki) }}
+{%- macro post_last_updater_and_creator_info(
+ post, min_rep_to_edit_wiki, karma_mode = None, badges_mode = None
+ ) -%}
+ {{ post_contributor_info(
+ post, "original_author", post.wiki, min_rep_to_edit_wiki,
+ karma_mode = karma_mode, badges_mode = badges_mode
+ )
+ }}
+ {{ post_contributor_info(
+ post, "last_updater", post.wiki, min_rep_to_edit_wiki,
+ karma_mode = karma_mode, badges_mode = badges_mode
+ )
+ }}
{%- endmacro -%}
-{%- macro post_contributor_info(post, contributor_type, is_wiki, wiki_min_rep) -%}
+{%- macro post_contributor_info(
+ post, contributor_type, is_wiki, wiki_min_rep,
+ karma_mode = None, badges_mode = None
+ ) -%}
{# there is a whole bunch of trickery here, probably indicative of
poor design of the data or methods on data objects #}
{% if contributor_type=="original_author" %}
@@ -72,7 +108,7 @@ poor design of the data or methods on data objects #}
{% else %}
{%- trans %}posted{% endtrans %}
{% endif %}
- <strong>{{post.added_at|diff_date}}</strong>
+ <strong>{{ timeago(post.added_at) }}</strong>
</p>
<img width="35" height="35"
src="{{'/images/wiki.png'|media}}"
@@ -92,12 +128,14 @@ poor design of the data or methods on data objects #}
{% trans %}posted{% endtrans %}
{% endif %}
{% if post.__class__.__name__ == 'PostRevision' %}
- <strong>{{post.revised_at|diff_date}}</strong>
+ <strong>{{ timeago(post.revised_at) }}</strong>
{% else %}
- <strong>{{post.added_at|diff_date}}</strong>
+ <strong>{{ timeago(post.added_at) }}</strong>
{% endif %}
</p>
- {{ post_contributor_avatar_and_credentials(post, post.author) }}
+ {{ post_contributor_avatar_and_credentials(
+ post, post.author, karma_mode = karma_mode, badges_mode = badges_mode
+ ) }}
{% endif %}
</div>
{% elif contributor_type=="last_updater" %}
@@ -119,10 +157,15 @@ poor design of the data or methods on data objects #}
{% else %}
href="{% url answer_revisions post.id %}"
{% endif %}
- >{% trans %}updated{% endtrans %} <strong>{{ last_edited_at|diff_date }}</strong></a>
+ >{% trans %}updated{% endtrans %} <strong>{{ timeago(last_edited_at) }}</strong></a>
</p>
{% if original_author != update_author or is_wiki %}
- {{ post_contributor_avatar_and_credentials(post, update_author) }}
+ {{
+ post_contributor_avatar_and_credentials(
+ post, update_author,
+ karma_mode = karma_mode, badges_mode = badges_mode
+ )
+ }}
{% endif %}
</div>
{% endif %}
@@ -178,6 +221,10 @@ poor design of the data or methods on data objects #}
</ul>
{%- endmacro -%}
+{%- macro user_group(group) -%}
+ {{ group.name|replace('-', ' ') }}
+{%- endmacro -%}
+
{# todo: remove the extra content argument to make its usage more explicit #}
{%- macro tag_widget(
tag,
@@ -306,7 +353,7 @@ for the purposes of the AJAX comment editor #}
<div class="comment-body">
{{comment.html}}
<a class="author" href="{{comment.author.get_profile_url()}}">{{comment.author.username}}</a>
- <span class="age">&nbsp;({{comment.added_at|diff_date}})</span>
+ <span class="age">&nbsp;({{ timeago(comment.added_at) }})</span>
<a id="post-{{comment.id}}-edit"
class="edit">{% trans %}edit{% endtrans %}</a>
</div>
@@ -422,7 +469,11 @@ for the purposes of the AJAX comment editor #}
answer {% if answer.accepted() %}accepted-answer{% endif %} {% if answer.author_id==question.author_id %} answered-by-owner{% endif %} {% if answer.deleted %}deleted{% endif -%}
{%- endmacro -%}
-{%- macro user_score_and_badge_summary(user) -%}
+{%- macro user_score_and_badge_summary(
+ user,
+ karma_mode = None,
+ badges_mode = None
+) -%}
{%include "widgets/user_score_and_badge_summary.html"%}
{%- endmacro -%}
@@ -457,8 +508,8 @@ answer {% if answer.accepted() %}accepted-answer{% endif %} {% if answer.author_
{% endif %}
{%- endmacro -%}
-{%- macro user_long_score_and_badge_summary(user) -%}
- {% include "widgets/user_long_score_and_badge_summary.html" %}
+{%- macro user_long_score_and_badge_summary(user, badges_mode = None) -%}
+ {%- include "widgets/user_long_score_and_badge_summary.html" -%}
{%- endmacro -%}
{%- macro user_country_flag(user) -%}
@@ -491,7 +542,10 @@ answer {% if answer.accepted() %}accepted-answer{% endif %} {% if answer.author_
{{ user_country_name_and_flag(user) }}
{%- endmacro -%}
-{%- macro user_list(users, profile_section = None) -%}
+{%- macro user_list(
+ users, profile_section = None, karma_mode = None, badges_mode = None
+ )
+-%}
{% include "widgets/user_list.html"%}
{%- endmacro -%}
@@ -641,3 +695,9 @@ answer {% if answer.accepted() %}accepted-answer{% endif %} {% if answer.author_
</a>
{% endif %}
{%- endmacro -%}
+
+{%- macro timeago(datetime_object) -%}
+ <abbr class="timeago" title="{{datetime_object.replace(microsecond=0)|add_tz_offset}}">
+ {{datetime_object.replace(microsecond=0)|add_tz_offset}}
+ </abbr>
+{%- endmacro -%}
diff --git a/askbot/skins/default/templates/main_page/headline.html b/askbot/skins/default/templates/main_page/headline.html
index 19dc7063..cc6f47a5 100644
--- a/askbot/skins/default/templates/main_page/headline.html
+++ b/askbot/skins/default/templates/main_page/headline.html
@@ -20,7 +20,7 @@
}}
</div>
{% endif %}
- {% if author_name or search_tags or query %}
+ {#% if author_name or search_tags or query %}
<p class="search-tips"><b>{% trans %}Search tips:{% endtrans %}</b>
{% if reset_method_count > 1 %}
{% if author_name %}
@@ -39,5 +39,5 @@
</p>
{% else %}
<p class="search-tips"><b>{% trans %}Search tip:{% endtrans %}</b> {% trans %}add tags and a query to focus your search{% endtrans %}</p>
- {% endif %}
+ {% endif %#}
{% endif %}
diff --git a/askbot/skins/default/templates/main_page/sidebar.html b/askbot/skins/default/templates/main_page/sidebar.html
index 9fb8fab9..fbfbdeb8 100644
--- a/askbot/skins/default/templates/main_page/sidebar.html
+++ b/askbot/skins/default/templates/main_page/sidebar.html
@@ -1,8 +1,10 @@
{% import "macros.html" as macros %}
+{% if settings.SIDEBAR_MAIN_HEADER %}
<div class="box">
{{ settings.SIDEBAR_MAIN_HEADER }}
</div>
+{% endif %}
{% if contributors and settings.SIDEBAR_MAIN_SHOW_AVATARS %}
{% include "widgets/contributors.html" %}
@@ -16,6 +18,8 @@
{% include "widgets/related_tags.html" %}
{% endif %}
+{% if settings.SIDEBARE_MAIN_FOOTER %}
<div class="box">
{{ settings.SIDEBAR_MAIN_FOOTER }}
</div>
+{% endif %}
diff --git a/askbot/skins/default/templates/meta/bottom_scripts.html b/askbot/skins/default/templates/meta/bottom_scripts.html
index 244cec21..a67f0fe8 100644
--- a/askbot/skins/default/templates/meta/bottom_scripts.html
+++ b/askbot/skins/default/templates/meta/bottom_scripts.html
@@ -30,8 +30,8 @@
></script>
<!-- History.js -->
<script type='text/javascript' src="{{"/js/jquery.history.js"|media }}"></script>
-<script type='text/javascript' src="{{"/js/utils.js"|media }}"></script>
<script type="text/javascript" src="{% url django.views.i18n.javascript_catalog %}"></script>
+<script type='text/javascript' src="{{"/js/utils.js"|media }}"></script>
{% if settings.ENABLE_MATHJAX %}
<script type='text/javascript' src="{{settings.MATHJAX_BASE_URL}}/MathJax.js">
MathJax.Hub.Config({
@@ -73,6 +73,7 @@
$('#validate_email_alert').click(function(){notify.close(true)})
notify.show();
{% endif %}
+ $('abbr.timeago').timeago();
</script>
{% if settings.USE_CUSTOM_JS %}
<script
diff --git a/askbot/skins/default/templates/question/sidebar.html b/askbot/skins/default/templates/question/sidebar.html
index 86a543c7..620268e1 100644
--- a/askbot/skins/default/templates/question/sidebar.html
+++ b/askbot/skins/default/templates/question/sidebar.html
@@ -1,4 +1,4 @@
-{% import "macros.html" as macros %}
+{% from "macros.html" import timeago %}
<div class="box">
{{ settings.SIDEBAR_QUESTION_HEADER }}
</div>
@@ -45,7 +45,7 @@
<div class="clearfix"></div>
<h2>{% trans %}Stats{% endtrans %}</h2>
<p>
- {% trans %}Asked{% endtrans %}: <strong title="{{ question.added_at }}">{{question.added_at|diff_date}}</strong>
+ {% trans %}Asked{% endtrans %}: {{ timeago(question.added_at) }}
</p>
<p>
{% trans %}Seen{% endtrans %}: <strong>{{ thread.view_count|intcomma }} {% trans %}times{% endtrans %}</strong>
diff --git a/askbot/skins/default/templates/reopen.html b/askbot/skins/default/templates/reopen.html
index d1ccc313..894fa3a0 100644
--- a/askbot/skins/default/templates/reopen.html
+++ b/askbot/skins/default/templates/reopen.html
@@ -1,4 +1,5 @@
{% extends "two_column_body.html" %}
+{% from "macros.html" import timeago %}
<!-- reopen.html -->
{% block title %}{% spaceless %}{% trans %}Reopen question{% endtrans %}{% endspaceless %}{% endblock %}
{% block content %}
@@ -16,7 +17,7 @@
{% trans %}Close reason:{% endtrans %} "<strong>{{question.thread.get_close_reason_display()}}</strong>".
</p>
<p>
- {% trans %}When:{% endtrans %} {{question.thread.closed_at|diff_date}}
+ {% trans %}When:{% endtrans %} {{ timeago(question.thread.closed_at) }}
</p>
<p>
{% trans %}Reopen this question?{% endtrans %}
diff --git a/askbot/skins/default/templates/user_profile/user.html b/askbot/skins/default/templates/user_profile/user.html
index 789c3c86..15e0622a 100644
--- a/askbot/skins/default/templates/user_profile/user.html
+++ b/askbot/skins/default/templates/user_profile/user.html
@@ -22,6 +22,9 @@
<script type="text/javascript">
var viewUserID = {{view_user.id}};
askbot['data']['viewUserName'] = '{{ view_user.username }}';
+ askbot['data']['viewUserId'] = {{view_user.id}};
+ askbot['urls']['edit_group_membership'] = '{% url edit_group_membership %}';
+ askbot['urls']['get_groups_list'] = '{% url get_groups_list %}';
</script>
{% if request.user|can_moderate_user(view_user) %}
<script type='text/javascript' src='{{"/js/jquery.form.js"|media}}'></script>
diff --git a/askbot/skins/default/templates/user_profile/user_inbox.html b/askbot/skins/default/templates/user_profile/user_inbox.html
index f70f1884..0852d6af 100644
--- a/askbot/skins/default/templates/user_profile/user_inbox.html
+++ b/askbot/skins/default/templates/user_profile/user_inbox.html
@@ -44,79 +44,32 @@ inbox_section - forum|flags
</a>
</div>
{% endif %}
- {% if inbox_section == 'forum' %}
<div id="re_tools">
<strong>{% trans %}select:{% endtrans %}</strong>
<a id="sel_all">{% trans %}all{% endtrans %}</a> |
<a id="sel_seen">{% trans %}seen{% endtrans %}</a> |
<a id="sel_new">{% trans %}new{% endtrans %}</a> |
<a id="sel_none">{% trans %}none{% endtrans %}</a><br />
- <button id="re_mark_seen">{% trans %}mark as seen{% endtrans %}</button>
- <button id="re_mark_new">{% trans %}mark as new{% endtrans %}</button>
- <button id="re_dismiss">{% trans %}dismiss{% endtrans %}</button>
+ {% if inbox_section == 'forum' %}
+ <button id="re_mark_seen">{% trans %}mark as seen{% endtrans %}</button>
+ <button id="re_mark_new">{% trans %}mark as new{% endtrans %}</button>
+ <button id="re_dismiss">{% trans %}dismiss{% endtrans %}</button>
+ {% else %}
+ <button id="re_remove_flag">{% trans %}remove flags/approve{% endtrans %}</button>
+ <button id="re_delete_post">{% trans %}delete post{% endtrans %}</button>
+ {% endif %}
</div>
- {% endif %}
- {% if inbox_section == 'flags' %}
- <div id="re_tools">
- <strong>{% trans %}select:{% endtrans %}</strong>
- <a id="sel_all">{% trans %}all{% endtrans %}</a> |
- <a id="sel_seen">{% trans %}seen{% endtrans %}</a> |
- <a id="sel_new">{% trans %}new{% endtrans %}</a> |
- <a id="sel_none">{% trans %}none{% endtrans %}</a><br />
- <button id="re_remove_flag">{% trans %}remove flags{% endtrans %}</button>
- <button id="re_close">{% trans %}close{% endtrans %}</button>
- <button id="re_delete_post">{% trans %}delete post{% endtrans %}</button>
- </div>
- {% endif %}
<div id="responses">
{% for response in responses %}
- <div class="response-parent">
- <p class="headline">
+ <div class="response-parent">
+ <p class="headline">
<strong>"{{ response.response_title.strip()|escape}}"</strong>
- </p>
- <div id="re_{{response.id}}" class="re{% if response.is_new %} new highlight{% else %} seen{% endif %}">
- <input type="checkbox" />
- <div class="face">
- {{ macros.gravatar(response.user, 48) }}
- </div>
- <a style="font-size:12px" href="{{ response.user.get_absolute_url() }}">{{ response.user.username }}</a>
- <a style="text-decoration:none;" href="{{ response.response_url }}">
- {{ response.response_type }}
- ({{ response.timestamp|diff_date(True) }}):<br/>
- {% if inbox_section != 'flags' %}
- {{ response.response_snippet }}
- {% endif %}
- </a>
- {% if inbox_section == 'flags' %}
- <a class="re_expand" href="{{ response.response_url }}">
- <div class="re_snippet">{{ response.response_snippet }}</div>
- <div class="re_content">{{ response.response_content }}</div></a>
- {% endif %}
- </div>
- {% if response.nested_responses %}
- {%for nested_response in response.nested_responses %}
- <div id="re_{{nested_response.id}}" class="re{% if nested_response.is_new %} new highlight{% else %} seen{% endif %}">
- <input type="checkbox" />
- <div class="face">
- {{ macros.gravatar(nested_response.user, 48) }}
- </div>
- <a style="font-size:12px" href="{{ nested_response.user.get_absolute_url() }}">{{ nested_response.user.username }}</a>
- <a style="text-decoration:none;" href="{{ nested_response.response_url }}">
- {{ nested_response.response_type }}
- ({{ nested_response.timestamp|diff_date(True) }}):<br/>
- {% if inbox_section != 'flags' %}
- {{ nested_response.response_snippet }}
- {% endif %}
- </a>
- {% if inbox_section == 'flags' %}
- <a class="re_expand" href="{{ nested_response.response_url }}">
- <div class="re_snippet">{{ nested_response.response_snippet }}</div>
- <div class="re_content">{{ nested_response.response_content }}</div></a>
- {% endif %}
- </div>
- {%endfor%}
- {%endif%}
- </div>
+ </p>
+ {{ macros.inbox_post_snippet(response, inbox_section) }}
+ {% for nested_response in response.nested_responses %}
+ {{ macros.inbox_post_snippet(nested_response, inbox_section) }}
+ {%endfor%}
+ </div>
{% endfor %}
</div>
</div>
diff --git a/askbot/skins/default/templates/user_profile/user_info.html b/askbot/skins/default/templates/user_profile/user_info.html
index 6d286c0a..18e74464 100644
--- a/askbot/skins/default/templates/user_profile/user_info.html
+++ b/askbot/skins/default/templates/user_profile/user_info.html
@@ -21,8 +21,10 @@
{% endif %}
{% endif %}
</div>
- <div class="scoreNumber">{{view_user.reputation|intcomma}}</div>
- <p><b style="color:#777;">{% trans %}karma{% endtrans %}</b></p>
+ {% if can_show_karma %}
+ <div class="scoreNumber">{{view_user.reputation|intcomma}}</div>
+ <p><b style="color:#777;">{% trans %}karma{% endtrans %}</b></p>
+ {% endif %}
{% if user_follow_feature_on %}
{{ macros.follow_user_toggle(visitor = request.user, subject = view_user) }}
{% endif %}
@@ -56,12 +58,12 @@
{% endif %}
<tr>
<td>{% trans %}member since{% endtrans %}</td>
- <td><strong>{{ view_user.date_joined|diff_date }}</strong></td>
+ <td><strong>{{ macros.timeago(view_user.date_joined) }}</strong></td>
</tr>
{% if view_user.last_seen %}
<tr>
<td>{% trans %}last seen{% endtrans %}</td>
- <td><strong title="{{ view_user.last_seen }}">{{view_user.last_seen|diff_date}}</strong></td>
+ <td><strong title="{{ view_user.last_seen }}">{{ macros.timeago(view_user.last_seen) }}</strong></td>
</tr>
{% endif %}
{% if view_user.website %}
diff --git a/askbot/skins/default/templates/user_profile/user_network.html b/askbot/skins/default/templates/user_profile/user_network.html
index 1fd2e06a..e6134e0c 100644
--- a/askbot/skins/default/templates/user_profile/user_network.html
+++ b/askbot/skins/default/templates/user_profile/user_network.html
@@ -8,11 +8,25 @@
{% if followed_users or followers %}
{% if followers %}
<h2>{% trans count=followers|length %}Followed by {{count}} person{% pluralize count %}Followed by {{count}} people{% endtrans %}</h2>
- {{ macros.user_list(followers, profile_section = 'network') }}
+ {{
+ macros.user_list(
+ followers,
+ profile_section = 'network',
+ karma_mode = settings.KARMA_MODE,
+ badges_mode = settings.BADGES_MODE
+ )
+ }}
{% endif %}
{% if followed_users %}
<h2>{% trans count=followed_users|length %}Following {{count}} person{% pluralize count %}Following {{count}} people{% endtrans %}</h2>
- {{ macros.user_list(followed_users, profile_section = 'network') }}
+ {{
+ macros.user_list(
+ followed_users,
+ profile_section = 'network',
+ karma_mode = settings.KARMA_MODE,
+ badges_mode = settings.BADGES_MODE
+ )
+ }}
{% endif %}
{% else %}
{% if request.user == view_user %}
diff --git a/askbot/skins/default/templates/user_profile/user_recent.html b/askbot/skins/default/templates/user_profile/user_recent.html
index bace94d8..8eae673d 100644
--- a/askbot/skins/default/templates/user_profile/user_recent.html
+++ b/askbot/skins/default/templates/user_profile/user_recent.html
@@ -1,4 +1,5 @@
{% extends "user_profile/user.html" %}
+{% from "macros.html" import timeago %}
<!-- user_recent.html -->
{% block profilesection %}
{% trans %}activity{% endtrans %}
@@ -7,7 +8,7 @@
<div style="padding-top:5px;font-size:13px;">
{% for act in activities %}
<div style="clear:both;line-height:20px" >
- <div style="width:180px;float:left">{{ act.time|diff_date(True) }}</div>
+ <div style="width:180px;float:left">{{ timeago(act.time) }}</div>
<div style="width:150px;float:left">
<span class="user-action-{{ act.type_id }}">{{ act.type }}</span>
</div>
diff --git a/askbot/skins/default/templates/user_profile/user_reputation.html b/askbot/skins/default/templates/user_profile/user_reputation.html
index 0deb2b97..1bb9b1ba 100644
--- a/askbot/skins/default/templates/user_profile/user_reputation.html
+++ b/askbot/skins/default/templates/user_profile/user_reputation.html
@@ -1,4 +1,5 @@
{% extends "user_profile/user.html" %}
+{% from "macros.html" import timeago %}
<!-- user_reputation.html -->
{% block profilesection %}
{% trans %}karma{% endtrans %}
@@ -17,7 +18,7 @@
<span class="karma-gained">{{ rep.positive }}</span>
<span class="karma-lost">{{ rep.negative }}</span>
{{ rep.get_explanation_snippet() }}
- <span class="small">({{rep.reputed_at|diff_date}})</span>
+ <span class="small">({{ timeago(rep.reputed_at) }})</span>
<div class="clean"></div>
</p>
{% endfor %}
diff --git a/askbot/skins/default/templates/user_profile/user_stats.html b/askbot/skins/default/templates/user_profile/user_stats.html
index 774550d8..6a1180ed 100644
--- a/askbot/skins/default/templates/user_profile/user_stats.html
+++ b/askbot/skins/default/templates/user_profile/user_stats.html
@@ -6,6 +6,24 @@
{% endblock %}
{% block usercontent %}
{% include "user_profile/user_info.html" %}
+ {% if settings.GROUPS_ENABLED %}
+ <div id="user-groups">
+ <h2>{% trans
+ username = view_user.username
+ %}{{username}}'s groups{% endtrans %}
+ </h2>
+ <ul>
+ {% if user_groups %}
+ {% for group in user_groups %}
+ <li>
+ {{ macros.user_group(group) }}
+ </li>
+ {% endfor %}
+ {% endif %}
+ </ul>
+ <a id="add-group">{% trans %}add group{% endtrans %}</a>
+ </div>
+ {% endif %}
<a name="questions"></a>
{% spaceless %}
<h2>{% trans counter=question_count %}<span class="count">{{counter}}</span> Question{% pluralize %}<span class="count">{{counter}}</span> Questions{% endtrans %}</h2>
@@ -92,6 +110,7 @@
</tr>
</table>
</div>
+ {% if settings.BADGES_MODE == 'public' %}
<a name="badges"></a>
{% spaceless %}
<h2>{% trans counter=total_badges %}<span class="count">{{counter}}</span> Badge{% pluralize %}<span class="count">{{counter}}</span> Badges{% endtrans %}</h2>
@@ -131,27 +150,6 @@
</tr>
</table>
</div>
-{% endblock %}
-{% block endjs %}
- {{ super() }}
- <script type="text/javascript">
- $(document).ready(function(){
- $('.badge-context-toggle').each(function(idx, elem){
- var context_list = $(elem).parent().next('ul');
- if (context_list.children().length > 0){
- $(elem).addClass('active');
- var toggle_display = function(){
- if (context_list.css('display') == 'none'){
- $('.badge-context-list').hide();{# hide all context lists #}
- context_list.show();
- } else {
- context_list.hide();
- }
- };
- $(elem).click(toggle_display);
- }
- });
- });
- </script>
+ {% endif %}
{% endblock %}
<!-- end user_stats.html -->
diff --git a/askbot/skins/default/templates/user_profile/user_tabs.html b/askbot/skins/default/templates/user_profile/user_tabs.html
index e6aead31..b8c56479 100644
--- a/askbot/skins/default/templates/user_profile/user_tabs.html
+++ b/askbot/skins/default/templates/user_profile/user_tabs.html
@@ -17,10 +17,12 @@
href="{% url user_profile view_user.id, view_user.username|slugify %}?sort=network"
><span>{% trans %}network{% endtrans %}</span></a>
{% endif %}
+ {% if can_show_karma %}
<a id="reputation" {% if tab_name=="reputation" %}class="on"{% endif %}
title="{% trans %}Graph of user karma{% endtrans %}"
href="{% url user_profile view_user.id, view_user.username|slugify %}?sort=reputation"
><span>{% trans %}karma{% endtrans %}</span></a>
+ {% endif %}
<a id="favorites" {% if tab_name=="favorites" %}class="on"{% endif %}
title="{% trans %}questions that user is following{% endtrans %}"
href="{% url user_profile view_user.id, view_user.username|slugify %}?sort=favorites"
diff --git a/askbot/skins/default/templates/user_profile/user_votes.html b/askbot/skins/default/templates/user_profile/user_votes.html
index 5111a580..b5fc4560 100644
--- a/askbot/skins/default/templates/user_profile/user_votes.html
+++ b/askbot/skins/default/templates/user_profile/user_votes.html
@@ -1,4 +1,5 @@
{% extends "user_profile/user.html" %}
+{% from "macros.html" import timeago %}
<!-- user_votes.html -->
{% block profilesection %}
{% trans %}votes{% endtrans %}
@@ -7,7 +8,7 @@
<div style="padding-top:5px;font-size:13px;">
{% for vote in votes %}
<div style="clear:both;line-height:20px" >
- <div style="width:150px;float:left">{{vote.voted_at|diff_date(True)}}</div>
+ <div style="width:150px;float:left">{{ timeago(vote.voted_at) }}</div>
<div style="width:30px;float:left">
{% if vote.vote==1 %}
<img src="{{"/images/vote-arrow-up-on-new.png"|media}}" title="{% trans %}upvote{% endtrans %}">
diff --git a/askbot/skins/default/templates/users.html b/askbot/skins/default/templates/users.html
index f2225772..1df8e1ea 100644
--- a/askbot/skins/default/templates/users.html
+++ b/askbot/skins/default/templates/users.html
@@ -3,31 +3,39 @@
<!-- users.html -->
{% block title %}{% spaceless %}{% trans %}Users{% endtrans %}{% endspaceless %}{% endblock %}
{% block content %}
-<h1 class="section-title">{% trans %}Users{% endtrans %}</h1>
-<div class="tabBar tabBar-user">
+<h1 class="section-title">
+{% if group %}
+ {% trans name = group.name|escape %}Users in group {{name}}{% endtrans %}
+{% else %}
+ {% trans %}Users{% endtrans %}
+{% endif %}
+</h1>
+<div class="tabBar">
<div class="tabsA">
<span class="label">{% trans %}Sort by &raquo;{% endtrans %}</span>
+ {% if settings.KARMA_MODE == 'public' %}
<a
id="sort_reputation"
- href="{% url users %}?sort=reputation"
+ href="{{ request.path }}?sort=reputation"
{% if tab_id == 'reputation' %}class="on"{% endif %}
title="{% trans %}see people with the highest reputation{% endtrans %}"
><span>{% trans %}karma{% endtrans %}</span></a>
+ {% endif %}
<a
id="sort_newest"
- href="{% url users %}?sort=newest"
+ href="{{ request.path }}?sort=newest"
{% if tab_id == 'newest' %}class="on"{% endif %}
class="off" title="{% trans %}see people who joined most recently{% endtrans %}"
><span>{% trans %}recent{% endtrans %}</span></a>
<a
id="sort_last"
- href="{% url users %}?sort=last"
+ href="{{ request.path }}?sort=last"
{% if tab_id == 'last' %}class="on"{% endif %}
class="off" title="{% trans %}see people who joined the site first{% endtrans %}"
><span>{% trans %}oldest{% endtrans %}<span></a>
<a
id="sort_user"
- href="{% url users %}?sort=user"
+ href="{{ request.path }}?sort=user"
{% if tab_id == 'user' %}class="on"{% endif %}
title="{% trans %}see people sorted by name{% endtrans %}"
><span>{% trans %}by username{% endtrans %}</span></a>
@@ -42,15 +50,85 @@
<span>{% trans %}Nothing found.{% endtrans %}</span>
{% endif %}
</p>
-{{ macros.user_list(users.object_list) }}
+{% if group %}
+ <div id="group-wiki-{{group.id}}" class="group-wiki">
+ <img class="group-logo"
+ {% if group.group_profile.logo_url %}
+ src="{{ group.group_profile.logo_url }}"
+ {% endif %}
+ />
+ <div class="content">
+ {% if group.tag_wiki %}
+ {{ group.tag_wiki.html }}
+ {% endif %}
+ </div>
+ <div class="clearfix"></div>
+ {% if request.user.is_authenticated() and request.user.is_administrator_or_moderator() %}
+ <a class="edit">
+ {% trans %}edit group description{% endtrans %}
+ </a> |
+ <a class="change_logo">
+ {% trans %}change logo{% endtrans %}
+ </a>
+ {% endif %}
+ </div>
+{% endif %}
+{{ macros.user_list(
+ users.object_list,
+ karma_mode = settings.KARMA_MODE, badges_mode = settings.BADGES_MODE
+ )
+}}
<div class="pager">
{{ macros.paginator(paginator_context) }}
</div>
{% endblock %}
{% block endjs %}
+ <script type='text/javascript'>
+ var Attacklab = Attacklab || {};
+ Attacklab.wmd = 1;{# a trick to launch wmd manually #}
+ askbot['urls']['upload'] = '{% url upload %}';
+ askbot['urls']['load_tag_wiki_text'] = '{% url load_tag_wiki_text %}';
+ askbot['urls']['save_tag_wiki_text'] = '{% url save_tag_wiki_text %}';
+ askbot['urls']['save_group_logo_url'] = '{% url save_group_logo_url %}';
+ </script>
+ <script type='text/javascript' src='{{"/js/editor.js"|media}}'></script>
+ <script type='text/javascript' src='{{"/js/wmd/showdown.js"|media}}'></script>
+ <script type='text/javascript' src='{{"/js/wmd/wmd.js"|media}}'></script>
+ <script type='text/javascript' src='{{"/js/jquery.validate.min.js"|media}}'></script>
+ <script src='{{"/js/post.js"|media}}' type='text/javascript'></script>
<script type="text/javascript">
//todo move javascript out
$().ready(function(){
+ if (askbot['data']['userIsAdminOrMod'] === true){
+ //todo: this is kind of Attacklab.init ... should not be here
+ Attacklab.loadEnv = function()
+ {
+ var mergeEnv = function(env)
+ {
+ if(!env)
+ {
+ return;
+ }
+
+ for(var key in env)
+ {
+ Attacklab.wmd_env[key] = env[key];
+ }
+ };
+
+ mergeEnv(Attacklab.wmd_defaults);
+ mergeEnv(Attacklab.account_options);
+ mergeEnv(top["wmd_options"]);
+ Attacklab.full = true;
+
+ var defaultButtons = "bold italic link blockquote code image ol ul heading hr";
+ Attacklab.wmd_env.buttons = Attacklab.wmd_env.buttons || defaultButtons;
+ };
+ Attacklab.loadEnv();
+ Attacklab.wmdBase();
+ var group_editor = new UserGroupProfileEditor();
+ group_editor.decorate($('#group-wiki-{{group.id}}'));
+ }
Hilite.exact = false;
Hilite.elementid = "main-body";
Hilite.debug_referrer = location.href;
diff --git a/askbot/skins/default/templates/widgets/meta_nav.html b/askbot/skins/default/templates/widgets/meta_nav.html
index b459b025..1b28c787 100644
--- a/askbot/skins/default/templates/widgets/meta_nav.html
+++ b/askbot/skins/default/templates/widgets/meta_nav.html
@@ -8,8 +8,18 @@
href="{% url users %}"
{% if active_tab == 'users' %}class="on"{% endif %}
>{% trans %}users{% endtrans %}</a>
+{% if settings.GROUPS_ENABLED %}
+<a
+ id="navGroups"
+ href="{% url groups %}"
+ {% if active_tab == 'groups' %}class="on"{% endif %}
+>{% trans %}groups{% endtrans %}
+</a>
+{% endif %}
+{% if settings.BADGES_MODE == 'public' %}
<a
id="navBadges"
href="{% url badges %}"
{% if active_tab == 'badges' %}class="on"{% endif %}
>{% trans %}badges{% endtrans %}</a>
+{% endif %}
diff --git a/askbot/skins/default/templates/widgets/question_summary.html b/askbot/skins/default/templates/widgets/question_summary.html
index 56154847..c6e7bc5d 100644
--- a/askbot/skins/default/templates/widgets/question_summary.html
+++ b/askbot/skins/default/templates/widgets/question_summary.html
@@ -1,4 +1,4 @@
-{% from "macros.html" import user_country_flag, tag_list_widget %}
+{% from "macros.html" import user_country_flag, tag_list_widget, timeago %}
<div class="short-summary{% if extra_class %} {{extra_class}}{% endif %}" id="question-{{question.id}}">
<div class="counts">
<div class="views
@@ -42,8 +42,7 @@
</div>
<div style="clear:both"></div>
<div class="userinfo">
- {# We have to kill microseconds below because InnoDB doesn't support them and all kinds of funny things happen in unit tests #}
- <span class="relativetime" title="{{thread.last_activity_at.replace(microsecond=0)}}">{{ thread.last_activity_at|diff_date }}</span>
+ {{ timeago(thread.last_activity_at) }}
{% if question.is_anonymous %}
<span class="anonymous">{{ thread.last_activity_by.get_anonymous_name() }}</span>
{% else %}
diff --git a/askbot/skins/default/templates/widgets/user_list.html b/askbot/skins/default/templates/widgets/user_list.html
index 7874946b..11f2ed50 100644
--- a/askbot/skins/default/templates/widgets/user_list.html
+++ b/askbot/skins/default/templates/widgets/user_list.html
@@ -8,7 +8,13 @@
<ul>
<li class="thumb">{{ gravatar(user, 32) }}</li>
<li><a href="{% url user_profile user.id, user.username|slugify %}{% if profile_section %}?sort={{profile_section}}{% endif %}">{{user.username}}</a>{{ user_country_flag(user) }}</li>
- <li>{{ user_score_and_badge_summary(user) }}</li>
+ <li>{{
+ user_score_and_badge_summary(
+ user,
+ karma_mode = karma_mode,
+ badges_mode = badges_mode
+ )
+ }}</li>
</ul>
</div>
{% if loop.index is divisibleby 7 %}
diff --git a/askbot/skins/default/templates/widgets/user_long_score_and_badge_summary.html b/askbot/skins/default/templates/widgets/user_long_score_and_badge_summary.html
index 121ae48f..efc59c55 100644
--- a/askbot/skins/default/templates/widgets/user_long_score_and_badge_summary.html
+++ b/askbot/skins/default/templates/widgets/user_long_score_and_badge_summary.html
@@ -1,21 +1,25 @@
+{%- if karma_mode != 'hidden' -%}
<a class="user-micro-info"
href="{{user.get_absolute_url()}}?sort=reputation"
>{% trans %}karma:{% endtrans %} {{user.reputation}}</a>
-{%- if user.gold or user.silver or user.bronze %}
-<a class="user-micro-info"
- href="{{user.get_absolute_url()}}#badges"
-><span title="{{user.get_badge_summary}}">{% trans %}badges:{% endtrans %}
- {% if user.gold %}
- <span class='badge1'>&#9679;</span>
- <span class="badgecount">{{user.gold}}</span>
- {% endif %}
- {% if user.silver %}
- <span class='badge2'>&#9679;</span>
- <span class="badgecount">{{user.silver}}</span>
- {% endif %}
- {% if user.bronze %}
- <span class='badge3'>&#9679;</span>
- <span class="badgecount">{{user.bronze}}</span>
+{%- endif -%}
+{% if badges_mode == 'public' %}
+ {%- if user.gold or user.silver or user.bronze %}
+ <a class="user-micro-info"
+ href="{{user.get_absolute_url()}}#badges"
+ ><span title="{{user.get_badge_summary}}">{% trans %}badges:{% endtrans %}
+ {% if user.gold %}
+ <span class='badge1'>&#9679;</span>
+ <span class="badgecount">{{user.gold}}</span>
+ {% endif %}
+ {% if user.silver %}
+ <span class='badge2'>&#9679;</span>
+ <span class="badgecount">{{user.silver}}</span>
+ {% endif %}
+ {% if user.bronze %}
+ <span class='badge3'>&#9679;</span>
+ <span class="badgecount">{{user.bronze}}</span>
+ {%- endif -%}
+ </span></a>
{%- endif -%}
-</span></a>
{%- endif -%}
diff --git a/askbot/skins/default/templates/widgets/user_navigation.html b/askbot/skins/default/templates/widgets/user_navigation.html
index e79a482e..eec7e628 100644
--- a/askbot/skins/default/templates/widgets/user_navigation.html
+++ b/askbot/skins/default/templates/widgets/user_navigation.html
@@ -1,9 +1,17 @@
-{% if request.user.is_authenticated() %}
+{%- if request.user.is_authenticated() -%}
<a href="{{ request.user.get_absolute_url() }}">{{ request.user.username }}</a>
<span class="user-info">
{{ macros.inbox_link(request.user) }}
{{ macros.moderation_items_link(request.user, moderation_items) }}
- ({{ macros.user_long_score_and_badge_summary(user) }})
+ {%-
+ if settings.KARMA_MODE != 'hidden' and settings.BADGES_MODE != 'hidden'
+ -%}
+ ({{ macros.user_long_score_and_badge_summary(
+ user,
+ badges_mode = settings.BADGES_MODE
+ )
+ }})
+ {%- endif -%}
</span>
{% if settings.USE_ASKBOT_LOGIN_SYSTEM %}
<a href="{{ settings.LOGOUT_URL }}?next={{ settings.LOGOUT_REDIRECT_URL }}">{% trans %}sign out{% endtrans %}</a>
diff --git a/askbot/skins/default/templates/widgets/user_score_and_badge_summary.html b/askbot/skins/default/templates/widgets/user_score_and_badge_summary.html
index 2f55b202..80d140db 100644
--- a/askbot/skins/default/templates/widgets/user_score_and_badge_summary.html
+++ b/askbot/skins/default/templates/widgets/user_score_and_badge_summary.html
@@ -1,19 +1,23 @@
+{% if karma_mode == 'public' %}
<span class="reputation-score"
title="{{user.get_karma_summary}}"
>{{user.reputation}}</span>
-{% if user.gold or user.silver or user.bronze %}
-<span title="{{user.get_badge_summary}}">
- {% if user.gold %}
- <span class='badge1'>&#9679;</span>
- <span class="badgecount">{{user.gold}}</span>
- {% endif %}
- {% if user.silver %}
- <span class='badge2'>&#9679;</span>
- <span class="badgecount">{{user.silver}}</span>
- {% endif %}
- {% if user.bronze %}
- <span class='badge3'>&#9679;</span>
- <span class="badgecount">{{user.bronze}}</span>
+{% endif %}
+{% if badges_mode == 'public' %}
+ {% if user.gold or user.silver or user.bronze %}
+ <span title="{{user.get_badge_summary}}">
+ {% if user.gold %}
+ <span class='badge1'>&#9679;</span>
+ <span class="badgecount">{{user.gold}}</span>
+ {% endif %}
+ {% if user.silver %}
+ <span class='badge2'>&#9679;</span>
+ <span class="badgecount">{{user.silver}}</span>
+ {% endif %}
+ {% if user.bronze %}
+ <span class='badge3'>&#9679;</span>
+ <span class="badgecount">{{user.bronze}}</span>
+ {% endif %}
+ </span>
{% endif %}
-</span>
{% endif %}
diff --git a/askbot/skins/utils.py b/askbot/skins/utils.py
index a07b1fa9..9f67d5c9 100644
--- a/askbot/skins/utils.py
+++ b/askbot/skins/utils.py
@@ -73,7 +73,9 @@ def get_path_to_skin(skin):
def get_skin_choices():
"""returns a tuple for use as a set of
choices in the form"""
- skin_names = list(reversed(get_available_skins().keys()))
+ available_skins = get_available_skins().keys()
+ available_skins.remove('common')
+ skin_names = list(reversed(available_skins))
return zip(skin_names, skin_names)
def resolve_skin_for_media(media=None, preferred_skin = None):
diff --git a/askbot/templatetags/extra_filters_jinja.py b/askbot/templatetags/extra_filters_jinja.py
index 8083657d..b03e4a89 100644
--- a/askbot/templatetags/extra_filters_jinja.py
+++ b/askbot/templatetags/extra_filters_jinja.py
@@ -1,4 +1,5 @@
import datetime
+import pytz
import re
import time
import urllib
@@ -35,6 +36,16 @@ def absolutize_urls_func(text):
return url_re4.sub(replacement, text)
absolutize_urls = register.filter(absolutize_urls_func)
+TIMEZONE_STR = pytz.timezone(
+ django_settings.TIME_ZONE
+ ).localize(
+ datetime.datetime.now()
+ ).strftime('%z')
+
+@register.filter
+def add_tz_offset(datetime_object):
+ return str(datetime_object) + ' ' + TIMEZONE_STR
+
@register.filter
def strip_path(url):
"""removes path part of the url"""
diff --git a/askbot/tests/form_tests.py b/askbot/tests/form_tests.py
index 4c67c1ff..654272b3 100644
--- a/askbot/tests/form_tests.py
+++ b/askbot/tests/form_tests.py
@@ -47,6 +47,7 @@ class AskByEmailFormTests(AskbotTestCase):
'subject': '[tag-one] where is titanic?',
'body_text': 'where is titanic?'
}
+
def test_subject_line(self):
"""loops through various forms of the subject line
and makes sure that tags and title are parsed out"""
diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py
index 5128c9e7..a1272b0d 100644
--- a/askbot/tests/reply_by_email_tests.py
+++ b/askbot/tests/reply_by_email_tests.py
@@ -1,15 +1,31 @@
from django.utils.translation import ugettext as _
from askbot.models import ReplyAddress
from askbot.lamson_handlers import PROCESS
+from askbot import const
from askbot.tests.utils import AskbotTestCase
from askbot.models import Post, PostRevision
+TEST_CONTENT = 'Test content'
+TEST_EMAIL_PARTS = (
+ ('body', TEST_CONTENT),
+)
+TEST_LONG_CONTENT = 'Test content' * 10
+TEST_LONG_EMAIL_PARTS = (
+ ('body', TEST_LONG_CONTENT),
+)
+
+class MockPart(object):
+ def __init__(self, body):
+ self.body = body
+ self.content_encoding = {'Content-Type':('text/plain',)}
+
class MockMessage(object):
def __init__(self, body, from_email):
self._body = body
+ self._part = MockPart(body)
self.From= from_email
def body(self):
@@ -17,7 +33,7 @@ class MockMessage(object):
def walk(self):
"""todo: add real file attachment"""
- return list()
+ return [self._part]
class EmailProcessingTests(AskbotTestCase):
@@ -46,9 +62,15 @@ class EmailProcessingTests(AskbotTestCase):
def test_process_correct_answer_comment(self):
addr = ReplyAddress.objects.create_new( self.answer, self.u1).address
- separator = _("======= Reply above this line. ====-=-=")
- msg = MockMessage("This is a test reply \n\nOn such and such someone\
- wrote something \n\n%s\nlorem ipsum "%(separator), "user1@domain.com")
+ reply_separator = const.REPLY_SEPARATOR_TEMPLATE % {
+ 'user_action': 'john did something',
+ '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),
+ "user1@domain.com"
+ )
PROCESS(msg, addr, '')
self.assertEquals(self.answer.comments.count(), 2)
self.assertEquals(self.answer.comments.all().order_by('-pk')[0].text.strip(), "This is a test reply")
@@ -86,31 +108,27 @@ class ReplyAddressModelTests(AskbotTestCase):
def test_create_answer_reply(self):
result = ReplyAddress.objects.create_new( self.answer, self.u1)
- post = result.create_reply("A test post")
+ post = result.create_reply(TEST_EMAIL_PARTS)
self.assertEquals(post.post_type, "comment")
- self.assertEquals(post.text, "A test post")
+ self.assertEquals(post.text, TEST_CONTENT)
self.assertEquals(self.answer.comments.count(), 2)
def test_create_comment_reply(self):
result = ReplyAddress.objects.create_new( self.comment, self.u1)
- post = result.create_reply("A test reply")
+ post = result.create_reply(TEST_EMAIL_PARTS)
self.assertEquals(post.post_type, "comment")
- self.assertEquals(post.text, "A test reply")
+ self.assertEquals(post.text, TEST_CONTENT)
self.assertEquals(self.answer.comments.count(), 2)
def test_create_question_comment_reply(self):
result = ReplyAddress.objects.create_new( self.question, self.u3)
- post = result.create_reply("A test post")
+ post = result.create_reply(TEST_EMAIL_PARTS)
self.assertEquals(post.post_type, "comment")
- self.assertEquals(post.text, "A test post")
+ self.assertEquals(post.text, TEST_CONTENT)
def test_create_question_answer_reply(self):
result = ReplyAddress.objects.create_new( self.question, self.u3)
- post = result.create_reply("A test post "* 10)
+ post = result.create_reply(TEST_LONG_EMAIL_PARTS)
self.assertEquals(post.post_type, "answer")
- self.assertEquals(post.text, "A test post "* 10)
-
-
-
-
+ self.assertEquals(post.text, TEST_LONG_CONTENT)
diff --git a/askbot/urls.py b/askbot/urls.py
index 1ab3ea5d..6ebdaf67 100644
--- a/askbot/urls.py
+++ b/askbot/urls.py
@@ -50,7 +50,7 @@ urlpatterns = patterns('',
url(
r'^%s(?P<id>\d+)/%s$' % (_('answers/'), _('revisions/')),
views.readers.revisions,
- kwargs = {'object_name': 'Answer'},
+ kwargs = {'post_type': 'answer'},
name='answer_revisions'
),
@@ -116,7 +116,7 @@ urlpatterns = patterns('',
url(
r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('revisions/')),
views.readers.revisions,
- kwargs = {'object_name': 'Question'},
+ kwargs = {'post_type': 'question'},
name='question_revisions'
),
url(
@@ -193,6 +193,26 @@ urlpatterns = patterns('',
name = 'get_tag_list'
),
url(
+ r'^load-tag-wiki-text/',
+ views.commands.load_tag_wiki_text,
+ name = 'load_tag_wiki_text'
+ ),
+ url(#ajax only
+ r'^save-tag-wiki-text/',
+ views.commands.save_tag_wiki_text,
+ name = 'save_tag_wiki_text'
+ ),
+ url(#ajax only
+ r'^save-group-logo-url/',
+ views.commands.save_group_logo_url,
+ name = 'save_group_logo_url'
+ ),
+ url(
+ r'^get-groups-list/',
+ views.commands.get_groups_list,
+ name = 'get_groups_list'
+ ),
+ url(
r'^swap-question-with-answer/',
views.commands.swap_question_with_answer,
name = 'swap_question_with_answer'
@@ -207,11 +227,17 @@ urlpatterns = patterns('',
views.users.users,
name='users'
),
+ url(
+ r'^%s%s(?P<group_id>\d+)/(?P<group_slug>.*)/$' % (_('users/'), _('by-group/')),
+ views.users.users,
+ kwargs = {'by_group': True},
+ name = 'users_by_group'
+ ),
#todo: rename as user_edit, b/c that's how template is named
url(
r'^%s(?P<id>\d+)/%s$' % (_('users/'), _('edit/')),
views.users.edit_user,
- name='edit_user'
+ name ='edit_user'
),
url(
r'^%s(?P<id>\d+)/(?P<slug>.+)/%s$' % (
@@ -228,6 +254,11 @@ urlpatterns = patterns('',
name='user_profile'
),
url(
+ r'^%s$' % _('groups/'),
+ views.users.groups,
+ name='groups'
+ ),
+ url(
r'^%s$' % _('users/update_has_custom_avatar/'),
views.users.update_has_custom_avatar,
name='user_update_has_custom_avatar'
@@ -252,6 +283,11 @@ urlpatterns = patterns('',
views.commands.manage_inbox,
name='manage_inbox'
),
+ url(#ajax only
+ r'^edit-group-membership/$',
+ views.commands.edit_group_membership,
+ name='edit_group_membership'
+ ),
url(
r'^feeds/(?P<url>.*)/$',
'django.contrib.syndication.views.feed',
diff --git a/askbot/utils/decorators.py b/askbot/utils/decorators.py
index 29e92645..c20b92e2 100644
--- a/askbot/utils/decorators.py
+++ b/askbot/utils/decorators.py
@@ -84,6 +84,8 @@ def ajax_only(view_func):
raise Http404
try:
data = view_func(request, *args, **kwargs)
+ if data is None:
+ data = {}
except Exception, e:
message = unicode(e)
if message == '':
diff --git a/askbot/utils/file_utils.py b/askbot/utils/file_utils.py
index daca1522..3793b5ce 100644
--- a/askbot/utils/file_utils.py
+++ b/askbot/utils/file_utils.py
@@ -5,7 +5,7 @@ import time
import urlparse
from django.core.files.storage import get_storage_class
-def store_file(file_object):
+def store_file(file_object, file_name_prefix = ''):
"""Creates an instance of django's file storage
object based on the file-like object,
returns the storage object, file name, file url
@@ -16,6 +16,7 @@ def store_file(file_object):
'.',
str(random.randint(0,100000))
) + os.path.splitext(file_object.name)[1].lower()
+ file_name = file_name_prefix + file_name
file_storage = get_storage_class()()
# use default storage to store file
diff --git a/askbot/utils/mail.py b/askbot/utils/mail.py
index e4fb7854..744c54de 100644
--- a/askbot/utils/mail.py
+++ b/askbot/utils/mail.py
@@ -7,6 +7,7 @@ import logging
from django.core import mail
from django.conf import settings as django_settings
from django.core.exceptions import PermissionDenied
+from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import string_concat
from askbot import exceptions
@@ -197,78 +198,107 @@ def bounce_email(email, subject, reason = None, body_text = None):
body_text = error_message
)
-def process_attachments(attachments):
- """saves file attachments and adds
-
- cheap way of dealing with the attachments
- just insert them inline, however it might
- be useful to keep track of the uploaded files separately
- and deal with them as with resources of their own value"""
- if attachments:
- content = ''
- for att in attachments:
- file_storage, file_name, file_url = store_file(att)
- chunk = '[%s](%s) ' % (att.name, file_url)
- file_extension = os.path.splitext(att.name)[1]
- #todo: this is a hack - use content type
- if file_extension.lower() in ('.png', '.jpg', '.gif'):
- chunk = '\n\n!' + chunk
- content += '\n\n' + chunk
- return content
+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 ''
+ return text
+def process_attachment(attachment):
+ """will save a single
+ attachment and return
+ link to file in the markdown format and the
+ file storage object
+ """
+ file_storage, file_name, file_url = store_file(attachment)
+ markdown_link = '[%s](%s) ' % (attachment.name, file_url)
+ file_extension = os.path.splitext(attachment.name)[1]
+ #todo: this is a hack - use content type
+ if file_extension.lower() in ('.png', '.jpg', '.jpeg', '.gif'):
+ markdown_link = '!' + markdown_link
+ return markdown_link, file_storage
+
+def process_parts(parts):
+ """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.
+ Returns ready to post body of the message and the list
+ of uploaded files.
+ """
+ body_markdown = ''
+ stored_files = list()
+ attachments_markdown = ''
+ for (part_type, content) in parts:
+ if part_type == 'attachment':
+ markdown, stored_file = process_attachment(content)
+ stored_files.append(stored_file)
+ attachments_markdown += '\n\n' + markdown
+ elif part_type == 'body':
+ body_markdown += '\n\n' + content
+ elif part_type == 'inline':
+ markdown, stored_file = process_attachment(content)
+ stored_files.append(stored_file)
+ body_markdown += markdown
+
+ #if the response separator is present -
+ #split the body with it, and discard the "so and so wrote:" part
+ body_markdown = extract_reply(body_markdown)
+ body_markdown += attachments_markdown
+ return body_markdown.strip(), stored_files
-def process_emailed_question(from_address, subject, body, attachments = None):
+
+def process_emailed_question(from_address, subject, parts):
"""posts question received by email or bounces the message"""
#a bunch of imports here, to avoid potential circular import issues
from askbot.forms import AskByEmailForm
from askbot.models import User
- data = {
- 'sender': from_address,
- 'subject': subject,
- 'body_text': body
- }
- form = AskByEmailForm(data)
- if form.is_valid():
- email_address = form.cleaned_data['email']
- try:
+
+ try:
+ #todo: delete uploaded files when posting by email fails!!!
+ body, stored_files = process_parts(parts)
+ data = {
+ 'sender': from_address,
+ 'subject': subject,
+ 'body_text': body
+ }
+ form = AskByEmailForm(data)
+ if form.is_valid():
+ email_address = form.cleaned_data['email']
user = User.objects.get(
email__iexact = email_address
)
- except User.DoesNotExist:
- bounce_email(email_address, subject, reason = 'unknown_user')
- except User.MultipleObjectsReturned:
- bounce_email(email_address, subject, reason = 'problem_posting')
-
- tagnames = form.cleaned_data['tagnames']
- title = form.cleaned_data['title']
- body_text = form.cleaned_data['body_text']
+ tagnames = form.cleaned_data['tagnames']
+ title = form.cleaned_data['title']
+ body_text = form.cleaned_data['body_text']
- try:
- body_text += process_attachments(attachments)
user.post_question(
title = title,
tags = tagnames,
- body_text = body_text
+ body_text = body_text,
+ by_email = True
)
- except PermissionDenied, error:
- bounce_email(
- email_address,
- subject,
- reason = 'permission_denied',
- body_text = unicode(error)
- )
- else:
- #error_list = list()
- #for field_errors in form.errors.values():
- # error_list.extend(field_errors)
+ else:
+ raise ValidationError()
+ except User.DoesNotExist:
+ bounce_email(email_address, subject, reason = 'unknown_user')
+ except User.MultipleObjectsReturned:
+ bounce_email(email_address, subject, reason = 'problem_posting')
+ except PermissionDenied, error:
+ bounce_email(
+ email_address,
+ subject,
+ reason = 'permission_denied',
+ body_text = unicode(error)
+ )
+ except ValidationError:
if from_address:
bounce_email(
from_address,
subject,
reason = 'problem_posting',
- #body_text = '\n*'.join(error_list)
)
diff --git a/askbot/views/commands.py b/askbot/views/commands.py
index 5d86d1a1..43df7718 100644
--- a/askbot/views/commands.py
+++ b/askbot/views/commands.py
@@ -41,7 +41,12 @@ def manage_inbox(request):
post_data = simplejson.loads(request.raw_post_data)
if request.user.is_authenticated():
activity_types = const.RESPONSE_ACTIVITY_TYPES_FOR_DISPLAY
- activity_types += (const.TYPE_ACTIVITY_MENTION, const.TYPE_ACTIVITY_MARK_OFFENSIVE,)
+ activity_types += (
+ const.TYPE_ACTIVITY_MENTION,
+ const.TYPE_ACTIVITY_MARK_OFFENSIVE,
+ const.TYPE_ACTIVITY_MODERATED_NEW_POST,
+ const.TYPE_ACTIVITY_MODERATED_POST_EDIT
+ )
user = request.user
memo_set = models.ActivityAuditStatus.objects.filter(
id__in = post_data['memo_list'],
@@ -58,20 +63,35 @@ def manage_inbox(request):
memo_set.update(status = models.ActivityAuditStatus.STATUS_SEEN)
elif action_type == 'remove_flag':
for memo in memo_set:
- request.user.flag_post(post = memo.activity.content_object, cancel_all = True)
- elif action_type == 'close':
- for memo in memo_set:
- if memo.activity.content_object.post_type == "question":
- request.user.close_question(question = memo.activity.content_object, reason = 7)
+ activity_type = memo.activity.activity_type
+ if activity_type == const.TYPE_ACTIVITY_MARK_OFFENSIVE:
+ request.user.flag_post(
+ post = memo.activity.content_object,
+ cancel_all = True
+ )
+ elif activity_type in \
+ (
+ const.TYPE_ACTIVITY_MODERATED_NEW_POST,
+ const.TYPE_ACTIVITY_MODERATED_POST_EDIT
+ ):
+ post_revision = memo.activity.content_object
+ request.user.approve_post_revision(post_revision)
memo.delete()
+
+ #elif action_type == 'close':
+ # for memo in memo_set:
+ # if memo.activity.content_object.post_type == "question":
+ # request.user.close_question(question = memo.activity.content_object, reason = 7)
+ # memo.delete()
elif action_type == 'delete_post':
for memo in memo_set:
- request.user.delete_post(post = memo.activity.content_object)
+ content_object = memo.activity.content_object
+ if isinstance(content_object, models.PostRevision):
+ post = content_object.post
+ else:
+ post = content_object
+ request.user.delete_post(post)
memo.delete()
- else:
- raise exceptions.PermissionDenied(
- _('Oops, apologies - there was some error')
- )
user.update_response_counts()
@@ -436,7 +456,49 @@ def get_tag_list(request):
'name', flat = True
)
output = '\n'.join(tag_names)
- return HttpResponse(output, mimetype = "text/plain")
+ return HttpResponse(output, mimetype = 'text/plain')
+
+@decorators.get_only
+def load_tag_wiki_text(request):
+ """returns text of the tag wiki in markdown format"""
+ tag = get_object_or_404(models.Tag, id = request.GET['tag_id'])
+ tag_wiki_text = getattr(tag.tag_wiki, 'text', '')
+ return HttpResponse(tag_wiki_text, mimetype = 'text/plain')
+
+@csrf.csrf_exempt
+@decorators.ajax_only
+@decorators.post_only
+def save_tag_wiki_text(request):
+ """if tag wiki text does not exist,
+ creates a new record, otherwise edits an existing
+ tag wiki record"""
+ form = forms.EditTagWikiForm(request.POST)
+ if form.is_valid():
+ tag_id = form.cleaned_data['tag_id']
+ text = form.cleaned_data['text']
+ tag = models.Tag.objects.get(id = tag_id)
+ if tag.tag_wiki:
+ request.user.edit_post(tag.tag_wiki, body_text = text)
+ tag_wiki = tag.tag_wiki
+ else:
+ tag_wiki = request.user.post_tag_wiki(tag, body_text = text)
+ return {'html': tag_wiki.html}
+ else:
+ raise ValueError('invalid post data')
+
+
+@decorators.get_only
+def get_groups_list(request):
+ """returns names of group tags
+ for the autocomplete function"""
+ group_names = models.Tag.group_tags.get_all().filter(
+ deleted = False
+ ).values_list(
+ 'name', flat = True
+ )
+ group_names = map(lambda v: v.replace('-', ' '), group_names)
+ output = '\n'.join(group_names)
+ return HttpResponse(output, mimetype = 'text/plain')
@csrf.csrf_protect
def subscribe_for_tags(request):
@@ -639,3 +701,67 @@ def read_message(request):#marks message a read
if request.user.is_authenticated():
request.user.delete_messages()
return HttpResponse('')
+
+
+@csrf.csrf_exempt
+@decorators.ajax_only
+@decorators.post_only
+def edit_group_membership(request):
+ if request.user.is_anonymous():
+ raise exceptions.PermissionDenied()
+
+ if not request.user.is_administrator_or_moderator():
+ raise exceptions.PermissionDenied(
+ _('Only moderators and administrators can change user groups')
+ )
+
+ form = forms.EditGroupMembershipForm(request.POST)
+ if form.is_valid():
+ group_name = form.cleaned_data['group_name']
+ user_id = form.cleaned_data['user_id']
+ try:
+ user = models.User.objects.get(id = user_id)
+ except models.User.DoesNotExist:
+ raise exceptions.PermissionDenied(
+ 'user with id %d not found' % user_id
+ )
+
+ action = form.cleaned_data['action']
+ if action == 'add':
+ group_params = {'group_name': group_name, 'user': user}
+ group = models.Tag.group_tags.get_or_create(**group_params)
+ request.user.edit_group_membership(user, group, 'add')
+ elif action == 'remove':
+ try:
+ group = models.Tag.group_tags.get_by_name(group_name = group_name)
+ request.user.edit_group_membership(user, group, 'remove')
+ except models.Tag.DoesNotExist:
+ raise exceptions.PermissionDenied()
+ else:
+ raise exceptions.PermissionDenied()
+ else:
+ raise exceptions.PermissionDenied()
+
+
+@csrf.csrf_exempt
+@decorators.ajax_only
+@decorators.post_only
+def save_group_logo_url(request):
+ if request.user.is_anonymous():
+ raise exceptions.PermissionDenied()
+
+ if not request.user.is_administrator_or_moderator():
+ raise exceptions.PermissionDenied(
+ _('Only moderators and administrators can change user groups')
+ )
+
+ form = forms.GroupLogoURLForm(request.POST)
+ if form.is_valid():
+ group_id = form.cleaned_data['group_id']
+ image_url = form.cleaned_data['image_url']
+ group = models.Tag.group_tags.get(id = group_id)
+ group.group_profile.logo_url = image_url
+ group.group_profile.save()
+ else:
+ raise ValueError('invalid data found when saving group logo')
+
diff --git a/askbot/views/meta.py b/askbot/views/meta.py
index 73cb494f..3e5a0b35 100644
--- a/askbot/views/meta.py
+++ b/askbot/views/meta.py
@@ -111,6 +111,8 @@ def privacy(request):
def badges(request):#user status/reputation system
#todo: supplement database data with the stuff from badges.py
+ if askbot_settings.BADGES_MODE != 'public':
+ raise Http404
known_badges = badge_data.BADGES.keys()
badges = BadgeData.objects.filter(slug__in = known_badges).order_by('slug')
my_badges = []
diff --git a/askbot/views/readers.py b/askbot/views/readers.py
index 2a81f5c2..9bb6495d 100644
--- a/askbot/views/readers.py
+++ b/askbot/views/readers.py
@@ -552,15 +552,12 @@ def question(request, id):#refactor - long subroutine. display question body, an
#print datetime.datetime.now() - before
return result
-def revisions(request, id, object_name=None):
- if object_name == 'Question':
- post = get_object_or_404(models.Post, post_type='question', id=id)
- else:
- post = get_object_or_404(models.Post, post_type='answer', id=id)
+def revisions(request, id, post_type = None):
+ assert post_type in ('question', 'answer')
+ post = get_object_or_404(models.Post, post_type=post_type, id=id)
revisions = list(models.PostRevision.objects.filter(post=post))
revisions.reverse()
for i, revision in enumerate(revisions):
- revision.html = revision.as_html()
if i == 0:
revision.diff = revisions[i].html
revision.summary = _('initial version')
diff --git a/askbot/views/users.py b/askbot/views/users.py
index 582bb2af..61273e95 100644
--- a/askbot/views/users.py
+++ b/askbot/views/users.py
@@ -55,9 +55,42 @@ def owner_or_moderator_required(f):
return f(request, profile_owner, context)
return wrapped_func
-def users(request):
+def users(request, by_group = False, group_id = None, group_slug = None):
+ """Users view, including listing of users by group"""
+ users = models.User.objects.all()
+ group = None
+ if by_group == True:
+ if askbot_settings.GROUPS_ENABLED == False:
+ raise Http404
+ if group_id:
+ if all((group_id, group_slug)) == False:
+ return HttpResponseRedirect('groups')
+ else:
+ try:
+ group = models.Tag.group_tags.get(id = group_id)
+ except models.Tag.DoesNotExist:
+ raise Http404
+ if group_slug == slugify(group.name):
+ users = models.User.objects.filter(
+ group_memberships__group__id = group_id
+ )
+ else:
+ group_page_url = reverse(
+ 'users_by_group',
+ kwargs = {
+ 'group_id': group.id,
+ 'group_slug': slugify(group.name)
+ }
+ )
+ return HttpResponseRedirect(group_page_url)
+
+
is_paginated = True
+
sortby = request.GET.get('sort', 'reputation')
+ if askbot_settings.KARMA_MODE == 'private' and sortby == 'reputation':
+ sortby = 'newest'
+
suser = request.REQUEST.get('query', "")
try:
page = int(request.GET.get('page', '1'))
@@ -76,23 +109,21 @@ def users(request):
order_by_parameter = '-reputation'
objects_list = Paginator(
- models.User.objects.all().order_by(
- order_by_parameter
- ),
+ users.order_by(order_by_parameter),
const.USERS_PAGE_SIZE
)
- base_url = reverse('users') + '?sort=%s&' % sortby
+ base_url = request.path + '?sort=%s&' % sortby
else:
sortby = "reputation"
objects_list = Paginator(
- models.User.objects.filter(
- username__icontains = suser
- ).order_by(
- '-reputation'
- ),
+ users.filter(
+ username__icontains = suser
+ ).order_by(
+ '-reputation'
+ ),
const.USERS_PAGE_SIZE
)
- base_url = reverse('users') + '?name=%s&sort=%s&' % (suser, sortby)
+ base_url = request.path + '?name=%s&sort=%s&' % (suser, sortby)
try:
users_page = objects_list.page(page)
@@ -114,6 +145,7 @@ def users(request):
'active_tab': 'users',
'page_class': 'users-page',
'users' : users_page,
+ 'group': group,
'suser' : suser,
'keywords' : suser,
'tab_id' : sortby,
@@ -275,6 +307,9 @@ def user_stats(request, user, context):
if request.user != user:
question_filter['is_anonymous'] = False
+ if askbot_settings.ENABLE_CONTENT_MODERATION:
+ question_filter['approved'] = True
+
#
# Questions
#
@@ -394,7 +429,7 @@ def user_stats(request, user, context):
'votes_total_per_day': votes_total,
'user_tags' : user_tags,
-
+ 'user_groups': models.Tag.group_tags.get_for_user(user = user),
'badges': badges,
'total_badges' : len(badges),
}
@@ -563,20 +598,32 @@ def user_responses(request, user, context):
as well as mentions of the user
user - the profile owner
+
+ the view has two sub-views - "forum" - i.e. responses
+ and "flags" - moderation items for mods only
"""
- section = 'forum'
- if request.user.is_moderator() or request.user.is_administrator():
- if 'section' in request.GET and request.GET['section'] == 'flags':
- section = 'flags'
+ #1) select activity types according to section
+ section = request.GET.get('section', 'forum')
+ if section == 'flags' and not\
+ (request.user.is_moderator() or request.user.is_administrator()):
+ raise Http404
if section == 'forum':
activity_types = const.RESPONSE_ACTIVITY_TYPES_FOR_DISPLAY
activity_types += (const.TYPE_ACTIVITY_MENTION,)
- else:
- assert(section == 'flags')
+ elif section == 'flags':
activity_types = (const.TYPE_ACTIVITY_MARK_OFFENSIVE,)
+ if askbot_settings.ENABLE_CONTENT_MODERATION:
+ activity_types += (
+ const.TYPE_ACTIVITY_MODERATED_NEW_POST,
+ const.TYPE_ACTIVITY_MODERATED_POST_EDIT
+ )
+ else:
+ raise Http404
+ #2) load the activity notifications according to activity types
+ #todo: insert pagination code here
memo_set = models.ActivityAuditStatus.objects.filter(
user = request.user,
activity__activity_type__in = activity_types
@@ -590,17 +637,17 @@ def user_responses(request, user, context):
'-activity__active_at'
)[:const.USER_VIEW_DATA_SIZE]
- #todo: insert pagination code here
-
+ #3) "package" data for the output
response_list = list()
for memo in memo_set:
+ #a monster query chain below
response = {
'id': memo.id,
'timestamp': memo.activity.active_at,
'user': memo.activity.user,
'is_new': memo.is_new(),
'response_url': memo.activity.get_absolute_url(),
- 'response_snippet': memo.activity.get_preview(),
+ 'response_snippet': memo.activity.get_snippet(),
'response_title': memo.activity.question.thread.title,
'response_type': memo.activity.get_activity_type_display(),
'response_id': memo.activity.question.id,
@@ -609,13 +656,14 @@ def user_responses(request, user, context):
}
response_list.append(response)
+ #4) sort by response id
response_list.sort(lambda x,y: cmp(y['response_id'], x['response_id']))
+
+ #5) group responses by thread (response_id is really the question post id)
last_response_id = None #flag to know if the response id is different
- last_response_index = None #flag to know if the response index in the list is different
filtered_response_list = list()
-
for i, response in enumerate(response_list):
- #todo: agrupate users
+ #todo: group responses by the user as well
if response['response_id'] == last_response_id:
original_response = dict.copy(filtered_response_list[len(filtered_response_list)-1])
original_response['nested_responses'].append(response)
@@ -623,12 +671,9 @@ def user_responses(request, user, context):
else:
filtered_response_list.append(response)
last_response_id = response['response_id']
- last_response_index = i
- response_list = filtered_response_list
-
- response_list.sort(lambda x,y: cmp(y['timestamp'], x['timestamp']))
- filtered_response_list = list()
+ #6) sort responses by time
+ filtered_response_list.sort(lambda x,y: cmp(y['timestamp'], x['timestamp']))
data = {
'active_tab':'users',
@@ -637,7 +682,7 @@ def user_responses(request, user, context):
'inbox_section':section,
'tab_description' : _('comments and answers to others questions'),
'page_title' : _('profile - responses'),
- 'responses' : response_list,
+ 'responses' : filtered_response_list,
}
context.update(data)
return render_into_skin('user_profile/user_inbox.html', context, request)
@@ -804,6 +849,20 @@ def user(request, id, slug=None, tab_name=None):
if not tab_name:
tab_name = request.GET.get('sort', 'stats')
+ if askbot_settings.KARMA_MODE == 'public':
+ can_show_karma = True
+ elif askbot_settings.KARMA_MODE == 'hidden':
+ can_show_karma = False
+ else:
+ if request.user.is_administrator_or_moderator() \
+ or request.user == profile_owner:
+ can_show_karma = True
+ else:
+ can_show_karma = False
+
+ if can_show_karma == False and tab_name == 'reputation':
+ raise Http404
+
user_view_func = USER_VIEW_CALL_TABLE.get(tab_name, user_stats)
search_state = SearchState( # Non-default SearchState with user data set
@@ -818,6 +877,7 @@ def user(request, id, slug=None, tab_name=None):
context = {
'view_user': profile_owner,
+ 'can_show_karma': can_show_karma,
'search_state': search_state,
'user_follow_feature_on': ('followit' in django_settings.INSTALLED_APPS),
}
@@ -833,3 +893,17 @@ def update_has_custom_avatar(request):
request.session['avatar_data_updated_at'] = datetime.datetime.now()
return HttpResponse(simplejson.dumps({'status':'ok'}), mimetype='application/json')
return HttpResponseForbidden()
+
+def groups(request, id = None, slug = None):
+ """output groups page
+ """
+ if askbot_settings.GROUPS_ENABLED == False:
+ raise Http404
+ groups = models.Tag.group_tags.get_all()
+ can_edit = request.user.is_authenticated() and \
+ request.user.is_administrator_or_moderator()
+ data = {
+ 'groups': groups,
+ 'can_edit': can_edit
+ }
+ return render_into_skin('groups.html', data, request)
diff --git a/askbot/views/writers.py b/askbot/views/writers.py
index d9a6f855..633c23f2 100644
--- a/askbot/views/writers.py
+++ b/askbot/views/writers.py
@@ -62,9 +62,13 @@ def upload(request):#ajax upload file to a question or answer
request.user.assert_can_upload_file()
+ #todo: build proper form validation
+ file_name_prefix = request.POST.get('file_name_prefix', '')
+ if file_name_prefix not in ('', 'group_logo_'):
+ raise exceptions.PermissionDenied('invalid upload file name prefix')
+
# check file type
f = request.FILES['file-upload']
-
#todo: extension checking should be replaced with mimetype checking
#and this must be part of the form validation
file_extension = os.path.splitext(f.name)[1].lower()
@@ -75,7 +79,9 @@ def upload(request):#ajax upload file to a question or answer
raise exceptions.PermissionDenied(msg)
# generate new file name and storage object
- file_storage, new_file_name, file_url = store_file(f)
+ file_storage, new_file_name, file_url = store_file(
+ f, file_name_prefix
+ )
# check file size
# byte
size = file_storage.size(new_file_name)
@@ -535,7 +541,7 @@ def __generate_comments_json(obj, user):#non-view generates json data for the po
comment_owner = comment.author
comment_data = {'id' : comment.id,
'object_id': obj.id,
- 'comment_age': diff_date(comment.added_at),
+ 'comment_added_at': str(comment.added_at.replace(microsecond = 0)),
'html': comment.html,
'user_display_name': comment_owner.username,
'user_url': comment_owner.get_profile_url(),
@@ -599,7 +605,7 @@ def edit_comment(request):
return {
'id' : comment_post.id,
'object_id': comment_post.parent.id,
- 'comment_age': diff_date(comment_post.added_at),
+ 'comment_added_at': str(comment_post.added_at.replace(microsecond = 0)),
'html': comment_post.html,
'user_display_name': comment_post.author.username,
'user_url': comment_post.author.get_profile_url(),
diff --git a/askbot_requirements.txt b/askbot_requirements.txt
index 301fb93e..7b619c36 100644
--- a/askbot_requirements.txt
+++ b/askbot_requirements.txt
@@ -18,3 +18,4 @@ django-followit
django-recaptcha-works
python-openid
pystache==0.3.1
+pytz
diff --git a/askbot_requirements_dev.txt b/askbot_requirements_dev.txt
index 199f0308..ada0d83e 100644
--- a/askbot_requirements_dev.txt
+++ b/askbot_requirements_dev.txt
@@ -19,3 +19,4 @@ django-recaptcha-works
python-openid
pystache==0.3.1
pylint
+pytz