diff options
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"> ({{comment.added_at|diff_date}})</span> + <span class="age"> ({{ 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 »{% 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'>●</span> - <span class="badgecount">{{user.gold}}</span> - {% endif %} - {% if user.silver %} - <span class='badge2'>●</span> - <span class="badgecount">{{user.silver}}</span> - {% endif %} - {% if user.bronze %} - <span class='badge3'>●</span> - <span class="badgecount">{{user.bronze}}</span> +{%- endif -%} +{% 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'>●</span> + <span class="badgecount">{{user.gold}}</span> + {% endif %} + {% if user.silver %} + <span class='badge2'>●</span> + <span class="badgecount">{{user.silver}}</span> + {% endif %} + {% if user.bronze %} + <span class='badge3'>●</span> + <span class="badgecount">{{user.bronze}}</span> + {%- endif -%} + </span></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'>●</span> - <span class="badgecount">{{user.gold}}</span> - {% endif %} - {% if user.silver %} - <span class='badge2'>●</span> - <span class="badgecount">{{user.silver}}</span> - {% endif %} - {% if user.bronze %} - <span class='badge3'>●</span> - <span class="badgecount">{{user.bronze}}</span> +{% endif %} +{% 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'>●</span> + <span class="badgecount">{{user.gold}}</span> + {% endif %} + {% if user.silver %} + <span class='badge2'>●</span> + <span class="badgecount">{{user.silver}}</span> + {% endif %} + {% if user.bronze %} + <span class='badge3'>●</span> + <span class="badgecount">{{user.bronze}}</span> + {% endif %} + </span> {% endif %} -</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 |