From 21e9a93fb624ecc0b9923062d732b90ef4a1e8cd Mon Sep 17 00:00:00 2001 From: Tomasz Zielinski Date: Tue, 17 Jan 2012 21:13:08 +0100 Subject: Removed dups from post.py --- askbot/models/post.py | 44 +------------------------------------------- 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/askbot/models/post.py b/askbot/models/post.py index d4482cf4..48abcd49 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -682,12 +682,6 @@ class Post(models.Model): raise NotImplementedError - - def delete(self, *args, **kwargs): - # WARNING: This is not called for batch deletions so watch out! - # TODO: Restore specialized Comment.delete() functionality! - super(Post, self).delete(*args, **kwargs) - def delete(self, **kwargs): """deletes comment and concomitant response activity records, as well as mention records, while preserving @@ -722,8 +716,6 @@ class Post(models.Model): super(Post, self).delete(**kwargs) - - def __unicode__(self): if self.is_question(): return self.thread.title @@ -733,12 +725,6 @@ class Post(models.Model): return self.text raise NotImplementedError - def is_answer(self): - return self.post_type == 'answer' - - def is_question(self): - return self.post_type == 'question' - def save(self, *args, **kwargs): if self.is_answer() and self.is_anonymous: raise ValueError('Answer cannot be anonymous!') @@ -1091,7 +1077,7 @@ class Post(models.Model): raise NotImplementedError def get_latest_revision(self): - return self.revisions.all().order_by('-revised_at')[0] + return self.revisions.order_by('-revised_at')[0] def get_latest_revision_number(self): if self.is_comment(): @@ -1255,22 +1241,6 @@ class Post(models.Model): return new_question - def get_page_number(self, answers = None): - """When question has many answers, answers are - paginated. This function returns number of the page - on which the answer will be shown, using the default - sort order. The result may depend on the visitor.""" - if self.is_question(): - return 1 - elif self.is_answer(): - order_number = 0 - for answer in answers: - if self == answer: - break - order_number += 1 - return int(order_number/const.ANSWERS_PAGE_SIZE) + 1 - raise NotImplementedError - def get_user_vote(self, user): if not self.is_answer(): raise NotImplementedError @@ -1608,15 +1578,6 @@ class Post(models.Model): return self.thread.title raise NotImplementedError - def accepted(self): - if self.is_answer(): - return self.question.thread.accepted_answer == self - raise NotImplementedError - - ##### - ##### - ##### - def accepted(self): if self.is_answer(): return self.thread.accepted_answer_id == self.id @@ -1647,9 +1608,6 @@ class Post(models.Model): raise NotImplementedError return self.parent.comments.filter(added_at__lt = self.added_at).count() + 1 - def get_latest_revision(self): - return self.revisions.order_by('-revised_at')[0] - def is_upvoted_by(self, user): from askbot.models.meta import Vote return Vote.objects.filter(user=user, voted_post=self, vote=Vote.VOTE_UP).exists() -- cgit v1.2.3-1-g7c22 From 4f0f77ca945a43bfd70767a30cd6819af1741cb8 Mon Sep 17 00:00:00 2001 From: Tomasz Zielinski Date: Tue, 17 Jan 2012 21:47:11 +0100 Subject: Removed old code --- askbot/models/post.py | 208 -------------------------------------------------- 1 file changed, 208 deletions(-) diff --git a/askbot/models/post.py b/askbot/models/post.py index 48abcd49..0474d257 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -80,214 +80,6 @@ class PostQuerySet(models.query.QuerySet): # #fallback to dumb title match search # return self.filter(thread__title__icontains=search_query) - # def run_advanced_search( - # self, - # request_user = None, - # search_state = None - # ): - # """all parameters are guaranteed to be clean - # however may not relate to database - in that case - # a relvant filter will be silently dropped - # """ - # #todo: same as for get_by_text_query - goes to Tread - # scope_selector = getattr( - # search_state, - # 'scope', - # const.DEFAULT_POST_SCOPE - # ) - # - # search_query = search_state.query - # tag_selector = search_state.tags - # author_selector = search_state.author - # - # import ipdb; ipdb.set_trace() - # - # sort_method = getattr( - # search_state, - # 'sort', - # const.DEFAULT_POST_SORT_METHOD - # ) - # qs = self.filter(deleted=False)#todo - add a possibility to see deleted questions - # - # #return metadata - # meta_data = {} - # if search_query: - # if search_state.stripped_query: - # qs = qs.get_by_text_query(search_state.stripped_query) - # #a patch for postgres search sort method - # if askbot.conf.should_show_sort_by_relevance(): - # if sort_method == 'relevance-desc': - # qs = qs.extra(order_by = ['-relevance',]) - # if search_state.query_title: - # qs = qs.filter(thread__title__icontains = search_state.query_title) - # if len(search_state.query_tags) > 0: - # qs = qs.filter(thread__tags__name__in = search_state.query_tags) - # if len(search_state.query_users) > 0: - # query_users = list() - # for username in search_state.query_users: - # try: - # user = User.objects.get(username__iexact = username) - # query_users.append(user) - # except User.DoesNotExist: - # pass - # if len(query_users) > 0: - # qs = qs.filter(author__in = query_users) - # - # if tag_selector: - # for tag in tag_selector: - # qs = qs.filter(thread__tags__name = tag) - # - # - # #have to import this at run time, otherwise there - # #a circular import dependency... - # from askbot.conf import settings as askbot_settings - # if scope_selector: - # if scope_selector == 'unanswered': - # qs = qs.filter(thread__closed = False)#do not show closed questions in unanswered section - # if askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_ANSWERS': - # qs = qs.filter(thread__answer_count=0)#todo: expand for different meanings of this - # elif askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_ACCEPTED_ANSWERS': - # qs = qs.filter(thread__accepted_answer__isnull=True) #answer_accepted=False - # elif askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_UPVOTED_ANSWERS': - # raise NotImplementedError() - # else: - # raise Exception('UNANSWERED_QUESTION_MEANING setting is wrong') - # elif scope_selector == 'favorite': - # favorite_filter = models.Q(thread__favorited_by = request_user) - # if 'followit' in settings.INSTALLED_APPS: - # followed_users = request_user.get_followed_users() - # favorite_filter |= models.Q(author__in = followed_users) - # favorite_filter |= models.Q(answers__author__in = followed_users) - # qs = qs.filter(favorite_filter) - # - # #user contributed questions & answers - # if author_selector: - # try: - # #todo maybe support selection by multiple authors - # u = User.objects.get(id=int(author_selector)) - # qs = qs.filter( - # models.Q(author=u, deleted=False) \ - # | models.Q(answers__author=u, answers__deleted=False) - # ) - # meta_data['author_name'] = u.username - # except User.DoesNotExist: - # meta_data['author_name'] = None - # - # #get users tag filters - # ignored_tag_names = None - # if request_user and request_user.is_authenticated(): - # uid_str = str(request_user.id) - # #mark questions tagged with interesting tags - # #a kind of fancy annotation, would be nice to avoid it - # interesting_tags = Tag.objects.filter( - # user_selections__user=request_user, - # user_selections__reason='good' - # ) - # ignored_tags = Tag.objects.filter( - # user_selections__user=request_user, - # user_selections__reason='bad' - # ) - # - # meta_data['interesting_tag_names'] = [tag.name for tag in interesting_tags] - # - # ignored_tag_names = [tag.name for tag in ignored_tags] - # meta_data['ignored_tag_names'] = ignored_tag_names - # - # if interesting_tags or request_user.has_interesting_wildcard_tags(): - # #expensive query - # if request_user.display_tag_filter_strategy == \ - # const.INCLUDE_INTERESTING: - # #filter by interesting tags only - # interesting_tag_filter = models.Q(thread__tags__in = interesting_tags) - # if request_user.has_interesting_wildcard_tags(): - # interesting_wildcards = request_user.interesting_tags.split() - # extra_interesting_tags = Tag.objects.get_by_wildcards( - # interesting_wildcards - # ) - # interesting_tag_filter |= models.Q(thread__tags__in = extra_interesting_tags) - # - # qs = qs.filter(interesting_tag_filter) - # else: - # pass - # #simply annotate interesting questions - ## qs = qs.extra( - ## select = SortedDict([ - ## ( - ## # TODO: [tags] Update this query so that it fetches tags from Thread - ## 'interesting_score', - ## 'SELECT COUNT(1) FROM askbot_markedtag, question_tags ' - ## + 'WHERE askbot_markedtag.user_id = %s ' - ## + 'AND askbot_markedtag.tag_id = question_tags.tag_id ' - ## + 'AND askbot_markedtag.reason = \'good\' ' - ## + 'AND question_tags.question_id = question.id' - ## ), - ## ]), - ## select_params = (uid_str,), - ## ) - # - # # get the list of interesting and ignored tags (interesting_tag_names, ignored_tag_names) = (None, None) - # - # if ignored_tags or request_user.has_ignored_wildcard_tags(): - # if request_user.display_tag_filter_strategy == const.EXCLUDE_IGNORED: - # #exclude ignored tags if the user wants to - # qs = qs.exclude(thread__tags__in=ignored_tags) - # if request_user.has_ignored_wildcard_tags(): - # ignored_wildcards = request_user.ignored_tags.split() - # extra_ignored_tags = Tag.objects.get_by_wildcards( - # ignored_wildcards - # ) - # qs = qs.exclude(thread__tags__in = extra_ignored_tags) - # else: - # pass - ## #annotate questions tagged with ignored tags - ## #expensive query - ## qs = qs.extra( - ## select = SortedDict([ - ## ( - ## 'ignored_score', - ## # TODO: [tags] Update this query so that it fetches tags from Thread - ## 'SELECT COUNT(1) ' - ## + 'FROM askbot_markedtag, question_tags ' - ## + 'WHERE askbot_markedtag.user_id = %s ' - ## + 'AND askbot_markedtag.tag_id = question_tags.tag_id ' - ## + 'AND askbot_markedtag.reason = \'bad\' ' - ## + 'AND question_tags.question_id = question.id' - ## ) - ## ]), - ## select_params = (uid_str, ) - ## ) - # - # if sort_method != 'relevance-desc': - # #relevance sort is set in the extra statement - # #only for postgresql - # orderby = QUESTION_ORDER_BY_MAP[sort_method] - # qs = qs.order_by(orderby) - # - # qs = qs.distinct() - # qs = qs.select_related( - # 'thread__last_activity_by__id', - # 'thread__last_activity_by__username', - # 'thread__last_activity_by__reputation', - # 'thread__last_activity_by__gold', - # 'thread__last_activity_by__silver', - # 'thread__last_activity_by__bronze', - # 'thread__last_activity_by__country', - # 'thread__last_activity_by__show_country', - # ) - # - # related_tags = Tag.objects.get_related_to_search( - # questions = qs, - # search_state = search_state, - # ignored_tag_names = ignored_tag_names - # ) - # if askbot_settings.USE_WILDCARD_TAGS == True \ - # and request_user.is_authenticated() == True: - # tagnames = request_user.interesting_tags - # meta_data['interesting_tag_names'].extend(tagnames.split()) - # tagnames = request_user.ignored_tags - # meta_data['ignored_tag_names'].extend(tagnames.split()) - # return qs, meta_data, related_tags - def added_between(self, start, end): """questions added between ``start`` and ``end`` timestamps""" #todo: goes to thread -- cgit v1.2.3-1-g7c22 From d49ceb5b144356865d2566336f2e90283de9d029 Mon Sep 17 00:00:00 2001 From: Tomasz Zielinski Date: Thu, 19 Jan 2012 18:22:01 +0100 Subject: Speed optimizations for the main page; bugfixes; added new tests and upgraded some old ones --- askbot/migrations/0107_added_db_indexes.py | 280 +++++++++++++++++++++ askbot/models/base.py | 7 + askbot/models/post.py | 8 +- askbot/models/question.py | 103 ++++---- askbot/models/tag.py | 37 +-- askbot/search/state_manager.py | 49 +++- .../templates/main_page/questions_loop.html | 6 +- .../templates/widgets/question_summary.html | 6 +- askbot/tests/page_load_tests.py | 133 +++++----- askbot/tests/post_model_tests.py | 154 +++++++++++- askbot/tests/search_state_tests.py | 120 +++++++-- askbot/views/readers.py | 36 +-- 12 files changed, 744 insertions(+), 195 deletions(-) create mode 100644 askbot/migrations/0107_added_db_indexes.py diff --git a/askbot/migrations/0107_added_db_indexes.py b/askbot/migrations/0107_added_db_indexes.py new file mode 100644 index 00000000..5893a41f --- /dev/null +++ b/askbot/migrations/0107_added_db_indexes.py @@ -0,0 +1,280 @@ +# encoding: 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 index on 'Post', fields ['post_type'] + db.create_index('askbot_post', ['post_type']) + + # Adding index on 'Post', fields ['deleted'] + db.create_index('askbot_post', ['deleted']) + + + def backwards(self, orm): + + # Removing index on 'Post', fields ['deleted'] + db.delete_index('askbot_post', ['deleted']) + + # Removing index on 'Post', fields ['post_type'] + db.delete_index('askbot_post', ['post_type']) + + + 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', 'db_index': 'True'}) + }, + '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.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.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'}), + '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']"}), + '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']"}), + '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/models/base.py b/askbot/models/base.py index 5f496d43..0686d50c 100644 --- a/askbot/models/base.py +++ b/askbot/models/base.py @@ -30,6 +30,13 @@ class BaseQuerySetManager(models.Manager): >>> objects = SomeManager() """ def __getattr__(self, attr, *args): + ## The following two lines fix the problem from this ticket: + ## https://code.djangoproject.com/ticket/15062#comment:6 + ## https://code.djangoproject.com/changeset/15220 + ## Queryset.only() seems to suffer from that on some occasions + if attr.startswith('_'): + raise AttributeError + ## try: return getattr(self.__class__, attr, *args) except AttributeError: diff --git a/askbot/models/post.py b/askbot/models/post.py index 0474d257..6cd9f7eb 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -230,7 +230,7 @@ class PostManager(BaseQuerySetManager): class Post(models.Model): - post_type = models.CharField(max_length=255) + post_type = models.CharField(max_length=255, db_index=True) old_question_id = models.PositiveIntegerField(null=True, blank=True, default=None, unique=True) old_answer_id = models.PositiveIntegerField(null=True, blank=True, default=None, unique=True) @@ -242,7 +242,7 @@ class Post(models.Model): author = models.ForeignKey(User, related_name='posts') added_at = models.DateTimeField(default=datetime.datetime.now) - deleted = models.BooleanField(default=False) + 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') @@ -452,8 +452,10 @@ class Post(models.Model): def is_comment(self): return self.post_type == 'comment' - def get_absolute_url(self, no_slug = False, question_post=None): + 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: + self._thread_cache = thread if self.is_answer(): if not question_post: question_post = self.thread._question_post() diff --git a/askbot/models/question.py b/askbot/models/question.py index c419cd6d..60e80ba4 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -1,5 +1,6 @@ import datetime import operator +from askbot.utils import mysql from django.conf import settings from django.db import models @@ -127,7 +128,7 @@ class ThreadManager(models.Manager): ) - def run_advanced_search(self, request_user, search_state, page_size): # TODO: !! review, fix, and write tests for this + def run_advanced_search(self, request_user, search_state): # TODO: !! review, fix, and write tests for this """ all parameters are guaranteed to be clean however may not relate to database - in that case @@ -136,7 +137,8 @@ class ThreadManager(models.Manager): """ from askbot.conf import settings as askbot_settings # Avoid circular import - qs = self.filter(posts__post_type='question', posts__deleted=False) # TODO: add a possibility to see deleted questions + # 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 meta_data = {} @@ -147,7 +149,7 @@ class ThreadManager(models.Manager): if search_state.query_users: query_users = User.objects.filter(username__in=search_state.query_users) if query_users: - qs = qs.filter(posts__post_type='question', posts__author__in=query_users) + qs = qs.filter(posts__post_type='question', posts__author__in=query_users) # TODO: unify with search_state.author ? tags = search_state.unified_tags() for tag in tags: @@ -183,7 +185,6 @@ class ThreadManager(models.Manager): meta_data['author_name'] = u.username #get users tag filters - ignored_tag_names = None if request_user and request_user.is_authenticated(): #mark questions tagged with interesting tags #a kind of fancy annotation, would be nice to avoid it @@ -191,9 +192,7 @@ class ThreadManager(models.Manager): ignored_tags = Tag.objects.filter(user_selections__user=request_user, user_selections__reason='bad') meta_data['interesting_tag_names'] = [tag.name for tag in interesting_tags] - - ignored_tag_names = [tag.name for tag in ignored_tags] - meta_data['ignored_tag_names'] = ignored_tag_names + meta_data['ignored_tag_names'] = [tag.name for tag in ignored_tags] if request_user.display_tag_filter_strategy == const.INCLUDE_INTERESTING and (interesting_tags or request_user.has_interesting_wildcard_tags()): #filter by interesting tags only @@ -213,47 +212,61 @@ class ThreadManager(models.Manager): extra_ignored_tags = Tag.objects.get_by_wildcards(ignored_wildcards) qs = qs.exclude(tags__in = extra_ignored_tags) - ### - # HACK: GO BACK To QUESTIONS, otherwise we cannot sort properly! - thread_ids = qs.values_list('id', flat = True) - qs_thread = qs - qs = Post.objects.filter(post_type='question', thread__id__in=thread_ids) - qs = qs.select_related('thread__last_activity_by') - - if search_state.sort == 'relevance-desc': - # TODO: askbot_thread.relevance is not available here, so we have to work around it. Ideas: - # * convert the whole questions() pipeline to Thread-s - # * ... - #qs = qs.extra(select={'relevance': 'askbot_thread.relevance'}, order_by=['-relevance',]) - pass - else: - QUESTION_ORDER_BY_MAP = { - 'age-desc': '-added_at', - 'age-asc': 'added_at', - 'activity-desc': '-thread__last_activity_at', - 'activity-asc': 'thread__last_activity_at', - 'answers-desc': '-thread__answer_count', - 'answers-asc': 'thread__answer_count', - 'votes-desc': '-score', - 'votes-asc': 'score', - } - orderby = QUESTION_ORDER_BY_MAP[search_state.sort] - qs = qs.order_by(orderby) - - related_tags = Tag.objects.get_related_to_search(questions = qs, page_size = page_size, ignored_tag_names = ignored_tag_names) # TODO: !! - - if askbot_settings.USE_WILDCARD_TAGS and request_user.is_authenticated(): - meta_data['interesting_tag_names'].extend(request_user.interesting_tags.split()) - meta_data['ignored_tag_names'].extend(request_user.ignored_tags.split()) + if askbot_settings.USE_WILDCARD_TAGS: + meta_data['interesting_tag_names'].extend(request_user.interesting_tags.split()) + meta_data['ignored_tag_names'].extend(request_user.ignored_tags.split()) + + QUESTION_ORDER_BY_MAP = { + 'age-desc': '-askbot_post.added_at', + 'age-asc': 'askbot_post.added_at', + 'activity-desc': '-last_activity_at', + 'activity-asc': 'last_activity_at', + 'answers-desc': '-answer_count', + 'answers-asc': 'answer_count', + 'votes-desc': '-askbot_post.score', + 'votes-asc': 'askbot_post.score', + + 'relevance-desc': '-relevance', # special Postgresql-specific ordering, 'relevance' quaso-column is added by get_for_query() + } + orderby = QUESTION_ORDER_BY_MAP[search_state.sort] + qs = qs.extra(order_by=[orderby]) + + # HACK: We add 'ordering_key' column as an alias and order by it, because when distict() is used, + # qs.extra(order_by=[orderby,]) is lost if only `orderby` column is from askbot_post! + # Removing distinct() from the queryset fixes the problem, but we have to use it here. + # UPDATE: Apparently we don't need distinct, the query don't duplicate Thread rows! + # qs = qs.extra(select={'ordering_key': orderby.lstrip('-')}, order_by=['-ordering_key' if orderby.startswith('-') else 'ordering_key']) + # qs = qs.distinct() + + qs = qs.only('id', 'title', 'view_count', 'answer_count', 'last_activity_at', 'last_activity_by', 'closed', 'tagnames', 'accepted_answer') + + #print qs.query + + return qs, meta_data + + def precache_view_data_hack(self, threads): + page_questions = Post.objects.filter(post_type='question', thread__in=[obj.id for obj in threads])\ + .only('id', 'thread', 'score', 'is_anonymous', 'summary', 'post_type', 'deleted') # pick only the used fields + page_question_map = {} + for pq in page_questions: + page_question_map[pq.thread_id] = pq + for thread in threads: + thread._question_cache = page_question_map[thread.id] - qs = qs.distinct() + last_activity_by_users = User.objects.filter(id__in=[obj.last_activity_by_id for obj in threads])\ + .only('id', 'username', 'country', 'show_country') + user_map = {} + for la_user in last_activity_by_users: + user_map[la_user.id] = la_user + for thread in threads: + thread._last_activity_by_cache = user_map[thread.last_activity_by_id] - return qs, meta_data, related_tags #todo: this function is similar to get_response_receivers - profile this function against the other one def get_thread_contributors(self, thread_list): """Returns query set of Thread contributors""" - u_id = Post.objects.filter(post_type__in=['question', 'answer'], thread__in=thread_list).values_list('author', flat=True) + # INFO: Evaluate this query to avoid subquery in the subsequent query below (At least MySQL can be awfully slow on subqueries) + u_id = list(Post.objects.filter(post_type__in=('question', 'answer'), thread__in=thread_list).values_list('author', flat=True)) #todo: this does not belong gere - here we select users with real faces #first and limit the number of users in the result for display @@ -300,7 +313,11 @@ class Thread(models.Model): app_label = 'askbot' def _question_post(self): - return Post.objects.get(post_type='question', thread=self) + post = getattr(self, '_question_cache', None) + if post: + return post + self._question_cache = Post.objects.get(post_type='question', thread=self) + return self._question_cache def get_absolute_url(self): return self._question_post().get_absolute_url() diff --git a/askbot/models/tag.py b/askbot/models/tag.py index 31ac9806..a13de661 100644 --- a/askbot/models/tag.py +++ b/askbot/models/tag.py @@ -66,42 +66,13 @@ class TagQuerySet(models.query.QuerySet): tag_filter |= models.Q(name__startswith = next_tag[:-1]) return self.filter(tag_filter) - def get_related_to_search(self, questions, page_size, ignored_tag_names): - """must return at least tag names, along with use counts - handle several cases to optimize the query performance - """ - - if questions.count() > page_size * 3: - """if we have too many questions or - search query is the most common - just return a list - of top tags""" - cheating = True - tags = Tag.objects.all().order_by('-used_count') - else: - cheating = False - #getting id's is necessary to avoid hitting a heavy query - #on entire selection of questions. We actually want - #the big questions query to hit only the page to be displayed - thread_id_list = questions.values_list('thread_id', flat=True) - tags = self.filter( - threads__id__in = thread_id_list, - ).annotate( - local_used_count=models.Count('id') - ).order_by( - '-local_used_count' - ) - + def get_related_to_search(self, threads, ignored_tag_names): + """Returns at least tag names, along with use counts""" + tags = self.filter(threads__in=threads).annotate(local_used_count=models.Count('id')).order_by('-local_used_count', 'name') if ignored_tag_names: tags = tags.exclude(name__in=ignored_tag_names) - tags = tags.exclude(deleted = True) - - tags = tags[:50]#magic number - if cheating: - for tag in tags: - tag.local_used_count = tag.used_count - - return tags + return list(tags[:50]) class TagManager(BaseQuerySetManager): diff --git a/askbot/search/state_manager.py b/askbot/search/state_manager.py index 211ce638..29e6484e 100644 --- a/askbot/search/state_manager.py +++ b/askbot/search/state_manager.py @@ -1,14 +1,15 @@ """Search state manager object""" import re +import urllib import copy from django.core import urlresolvers -from django.utils.http import urlquote, urlencode +from django.utils.http import urlencode +from django.utils.encoding import smart_str import askbot import askbot.conf from askbot import const -from askbot.conf import settings as askbot_settings from askbot.utils.functions import strip_plus @@ -122,11 +123,13 @@ class SearchState(object): if self.page == 0: # in case someone likes jokes :) self.page = 1 + self._questions_url = urlresolvers.reverse('questions') + def __str__(self): return self.query_string() def full_url(self): - return urlresolvers.reverse('questions') + self.query_string() + return self._questions_url + self.query_string() def ask_query_string(self): # TODO: test me """returns string to prepopulate title field on the "Ask your question" page""" @@ -158,27 +161,47 @@ class SearchState(object): def query_string(self): lst = [ - 'scope:%s' % self.scope, - 'sort:%s' % self.sort + 'scope:' + self.scope, + 'sort:' + self.sort ] if self.query: - lst.append('query:%s' % urlquote(self.query, safe=self.SAFE_CHARS)) + lst.append('query:' + urllib.quote(smart_str(self.query), safe=self.SAFE_CHARS)) if self.tags: - lst.append('tags:%s' % urlquote(const.TAG_SEP.join(self.tags), safe=self.SAFE_CHARS)) + lst.append('tags:' + urllib.quote(smart_str(const.TAG_SEP.join(self.tags)), safe=self.SAFE_CHARS)) if self.author: - lst.append('author:%d' % self.author) + lst.append('author:' + str(self.author)) if self.page: - lst.append('page:%d' % self.page) + lst.append('page:' + str(self.page)) return '/'.join(lst) + '/' - def deepcopy(self): + def deepcopy(self): # TODO: test me "Used to contruct a new SearchState for manipulation, e.g. for adding/removing tags" - return copy.deepcopy(self) + ss = copy.copy(self) #SearchState.get_empty() + + #ss.scope = self.scope + #ss.sort = self.sort + #ss.query = self.query + if ss.tags is not None: # it's important to test against None, because empty lists should also be cloned! + ss.tags = ss.tags[:] # create a copy + #ss.author = self.author + #ss.page = self.page + + #ss.stripped_query = self.stripped_query + if ss.query_tags: # Here we don't have empty lists, only None + ss.query_tags = ss.query_tags[:] + if ss.query_users: + ss.query_users = ss.query_users[:] + #ss.query_title = self.query_title + + #ss._questions_url = self._questions_url + + return ss def add_tag(self, tag): ss = self.deepcopy() - ss.tags.append(tag) - ss.page = 1 # state change causes page reset + if tag not in ss.tags: + ss.tags.append(tag) + ss.page = 1 # state change causes page reset return ss def remove_author(self): diff --git a/askbot/skins/default/templates/main_page/questions_loop.html b/askbot/skins/default/templates/main_page/questions_loop.html index 7e924e63..6d83032c 100644 --- a/askbot/skins/default/templates/main_page/questions_loop.html +++ b/askbot/skins/default/templates/main_page/questions_loop.html @@ -1,9 +1,9 @@ {% import "macros.html" as macros %} {# cache 0 "questions" questions search_tags scope sort query context.page language_code #} -{% for question_post in questions.object_list %} - {{macros.question_summary(question_post.thread, question_post, search_state=search_state)}} +{% for thread in threads.object_list %} + {{macros.question_summary(thread, thread._question_post(), search_state=search_state)}} {% endfor %} -{% if questions.object_list|length == 0 %} +{% if threads.object_list|length == 0 %} {% include "main_page/nothing_found.html" %} {% else %}
diff --git a/askbot/skins/default/templates/widgets/question_summary.html b/askbot/skins/default/templates/widgets/question_summary.html index feebd27f..6ab59de5 100644 --- a/askbot/skins/default/templates/widgets/question_summary.html +++ b/askbot/skins/default/templates/widgets/question_summary.html @@ -46,12 +46,12 @@ {% if question.is_anonymous %} {{ thread.last_activity_by.get_anonymous_name() }} {% else %} - {{thread.last_activity_by.username}}{{ user_country_flag(thread.last_activity_by) }} - {#{user_score_and_badge_summary(thread.last_activity_by)}#} + {{thread.last_activity_by.username}} {{ user_country_flag(thread.last_activity_by) }} + {#{user_score_and_badge_summary(thread.last_activity_by)}#} {% endif %}
-

{{thread.get_title(question)|escape}}

+

{{thread.get_title(question)|escape}}

{{ tag_list_widget(thread.get_tag_names(), search_state=search_state) }} diff --git a/askbot/tests/page_load_tests.py b/askbot/tests/page_load_tests.py index f423230b..8abde42f 100644 --- a/askbot/tests/page_load_tests.py +++ b/askbot/tests/page_load_tests.py @@ -1,3 +1,4 @@ +from askbot.search.state_manager import SearchState from django.test import TestCase from django.test import signals from django.template import defaultfilters @@ -50,8 +51,11 @@ class PageLoadTestCase(AskbotTestCase): self, url_name, status_code=200, template=None, kwargs={}, redirect_url=None, follow=False, - data={}): - url = reverse(url_name, kwargs=kwargs) + data={}, plain_url_passed=False): + if plain_url_passed: + url = url_name + else: + url = reverse(url_name, kwargs=kwargs) if status_code == 302: url_info = 'redirecting to LOGIN_URL in closed_mode: %s' % url else: @@ -161,76 +165,81 @@ class PageLoadTestCase(AskbotTestCase): ) #todo: test different sort methods and scopes self.try_url( - 'questions', - status_code=status_code, - template='main_page.html' - ) - self.try_url( - 'questions', - status_code=status_code, - data={'start_over':'true'}, - template='main_page.html' - ) + 'questions', + status_code=status_code, + template='main_page.html' + ) self.try_url( - 'questions', - status_code=status_code, - data={'scope':'unanswered'}, - template='main_page.html' - ) + url_name=reverse('questions') + SearchState.get_empty().change_scope('unanswered').query_string(), + plain_url_passed=True, + + status_code=status_code, + template='main_page.html', + ) self.try_url( - 'questions', - status_code=status_code, - data={'scope':'favorite'}, - template='main_page.html' - ) + url_name=reverse('questions') + SearchState.get_empty().change_scope('favorite').query_string(), + plain_url_passed=True, + + status_code=status_code, + template='main_page.html' + ) self.try_url( - 'questions', - status_code=status_code, - data={'scope':'unanswered', 'sort':'age-desc'}, - template='main_page.html' - ) + url_name=reverse('questions') + SearchState.get_empty().change_scope('unanswered').change_sort('age-desc').query_string(), + plain_url_passed=True, + + status_code=status_code, + template='main_page.html' + ) self.try_url( - 'questions', - status_code=status_code, - data={'scope':'unanswered', 'sort':'age-asc'}, - template='main_page.html' - ) + url_name=reverse('questions') + SearchState.get_empty().change_scope('unanswered').change_sort('age-asc').query_string(), + plain_url_passed=True, + + status_code=status_code, + template='main_page.html' + ) self.try_url( - 'questions', - status_code=status_code, - data={'scope':'unanswered', 'sort':'activity-desc'}, - template='main_page.html' - ) + url_name=reverse('questions') + SearchState.get_empty().change_scope('unanswered').change_sort('activity-desc').query_string(), + plain_url_passed=True, + + status_code=status_code, + template='main_page.html' + ) self.try_url( - 'questions', - status_code=status_code, - data={'scope':'unanswered', 'sort':'activity-asc'}, - template='main_page.html' - ) + url_name=reverse('questions') + SearchState.get_empty().change_scope('unanswered').change_sort('activity-asc').query_string(), + plain_url_passed=True, + + status_code=status_code, + template='main_page.html' + ) self.try_url( - 'questions', - status_code=status_code, - data={'sort':'answers-desc'}, - template='main_page.html' - ) + url_name=reverse('questions') + SearchState.get_empty().change_sort('answers-desc').query_string(), + plain_url_passed=True, + + status_code=status_code, + template='main_page.html' + ) self.try_url( - 'questions', - status_code=status_code, - data={'sort':'answers-asc'}, - template='main_page.html' - ) + url_name=reverse('questions') + SearchState.get_empty().change_sort('answers-asc').query_string(), + plain_url_passed=True, + + status_code=status_code, + template='main_page.html' + ) self.try_url( - 'questions', - status_code=status_code, - data={'sort':'votes-desc'}, - template='main_page.html' - ) + url_name=reverse('questions') + SearchState.get_empty().change_sort('votes-desc').query_string(), + plain_url_passed=True, + + status_code=status_code, + template='main_page.html' + ) self.try_url( - 'questions', - status_code=status_code, - data={'sort':'votes-asc'}, - template='main_page.html' - ) + url_name=reverse('questions') + SearchState.get_empty().change_sort('votes-asc').query_string(), + plain_url_passed=True, + + status_code=status_code, + template='main_page.html' + ) + self.try_url( 'question', status_code=status_code, diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py index e11d2e81..7e66d144 100644 --- a/askbot/tests/post_model_tests.py +++ b/askbot/tests/post_model_tests.py @@ -1,8 +1,11 @@ import datetime +from operator import attrgetter +from askbot.search.state_manager import SearchState +from django.contrib.auth.models import User from django.core.exceptions import ValidationError from askbot.tests.utils import AskbotTestCase -from askbot.models import Post, PostRevision +from askbot.models import Post, PostRevision, Thread, Tag class PostModelTests(AskbotTestCase): @@ -149,4 +152,151 @@ class PostModelTests(AskbotTestCase): Post.objects.precache_comments(for_posts=[q], visitor=self.user) self.assertListEqual([c3, c2, c1], q._cached_comments) - del self.user \ No newline at end of file + del self.user + + def test_cached_get_absolute_url_1(self): + th = lambda:1 + th.title = 'lala-x-lala' + p = Post(id=3, post_type='question') + p._thread_cache = th # cannot assign non-Thread instance directly + self.assertEqual('/question/3/lala-x-lala', p.get_absolute_url(thread=th)) + self.assertTrue(p._thread_cache is th) + self.assertEqual('/question/3/lala-x-lala', p.get_absolute_url(thread=th)) + + def test_cached_get_absolute_url_2(self): + p = Post(id=3, post_type='question') + th = lambda:1 + th.title = 'lala-x-lala' + self.assertEqual('/question/3/lala-x-lala', p.get_absolute_url(thread=th)) + self.assertTrue(p._thread_cache is th) + self.assertEqual('/question/3/lala-x-lala', p.get_absolute_url(thread=th)) + + +class ThreadTagModelsTests(AskbotTestCase): + + # TODO: Use rich test data like page load test cases ? + + def setUp(self): + self.create_user() + user2 = self.create_user(username='user2') + user3 = self.create_user(username='user3') + self.q1 = self.post_question(tags='tag1 tag2 tag3') + self.q2 = self.post_question(tags='tag3 tag4 tag5') + self.q3 = self.post_question(tags='tag6', user=user2) + self.q4 = self.post_question(tags='tag1 tag2 tag3 tag4 tag5 tag6', user=user3) + + def test_related_tags(self): + tags = Tag.objects.get_related_to_search(threads=[self.q1.thread, self.q2.thread], ignored_tag_names=[]) + self.assertListEqual(['tag3', 'tag1', 'tag2', 'tag4', 'tag5'], [t.name for t in tags]) + self.assertListEqual([2, 1, 1, 1, 1], [t.local_used_count for t in tags]) + self.assertListEqual([3, 2, 2, 2, 2], [t.used_count for t in tags]) + + tags = Tag.objects.get_related_to_search(threads=[self.q1.thread, self.q2.thread], ignored_tag_names=['tag3', 'tag5']) + self.assertListEqual(['tag1', 'tag2', 'tag4'], [t.name for t in tags]) + self.assertListEqual([1, 1, 1], [t.local_used_count for t in tags]) + self.assertListEqual([2, 2, 2], [t.used_count for t in tags]) + + tags = Tag.objects.get_related_to_search(threads=[self.q3.thread], ignored_tag_names=[]) + self.assertListEqual(['tag6'], [t.name for t in tags]) + self.assertListEqual([1], [t.local_used_count for t in tags]) + self.assertListEqual([2], [t.used_count for t in tags]) + + tags = Tag.objects.get_related_to_search(threads=[self.q3.thread], ignored_tag_names=['tag1']) + self.assertListEqual(['tag6'], [t.name for t in tags]) + self.assertListEqual([1], [t.local_used_count for t in tags]) + self.assertListEqual([2], [t.used_count for t in tags]) + + tags = Tag.objects.get_related_to_search(threads=[self.q3.thread], ignored_tag_names=['tag6']) + self.assertListEqual([], [t.name for t in tags]) + + tags = Tag.objects.get_related_to_search(threads=[self.q1.thread, self.q2.thread, self.q4], ignored_tag_names=['tag2']) + self.assertListEqual(['tag3', 'tag1', 'tag4', 'tag5', 'tag6'], [t.name for t in tags]) + self.assertListEqual([3, 2, 2, 2, 1], [t.local_used_count for t in tags]) + self.assertListEqual([3, 2, 2, 2, 2], [t.used_count for t in tags]) + + def test_run_adv_search_1(self): + ss = SearchState.get_empty() + qs, meta_data = Thread.objects.run_advanced_search(request_user=self.user, search_state=ss) + self.assertEqual(4, qs.count()) + + def test_run_adv_search_ANDing_tags(self): + ss = SearchState.get_empty() + qs, meta_data = Thread.objects.run_advanced_search(request_user=self.user, search_state=ss.add_tag('tag1')) + self.assertEqual(2, qs.count()) + + qs, meta_data = Thread.objects.run_advanced_search(request_user=self.user, search_state=ss.add_tag('tag1').add_tag('tag3')) + self.assertEqual(2, qs.count()) + + qs, meta_data = Thread.objects.run_advanced_search(request_user=self.user, search_state=ss.add_tag('tag1').add_tag('tag3').add_tag('tag6')) + self.assertEqual(1, qs.count()) + + ss = SearchState(scope=None, sort=None, query="#tag3", tags='tag1, tag6', author=None, page=None, user_logged_in=None) + qs, meta_data = Thread.objects.run_advanced_search(request_user=self.user, search_state=ss) + self.assertEqual(1, qs.count()) + + def test_run_adv_search_query_author(self): + ss = SearchState(scope=None, sort=None, query="@user", tags=None, author=None, page=None, user_logged_in=None) + qs, meta_data = Thread.objects.run_advanced_search(request_user=self.user, search_state=ss) + self.assertEqual(2, len(qs)) + self.assertEqual(self.q1.thread_id, min(qs[0].id, qs[1].id)) + self.assertEqual(self.q2.thread_id, max(qs[0].id, qs[1].id)) + + ss = SearchState(scope=None, sort=None, query="@user2", tags=None, author=None, page=None, user_logged_in=None) + qs, meta_data = Thread.objects.run_advanced_search(request_user=self.user, search_state=ss) + self.assertEqual(1, len(qs)) + self.assertEqual(self.q3.thread_id, qs[0].id) + + ss = SearchState(scope=None, sort=None, query="@user3", tags=None, author=None, page=None, user_logged_in=None) + qs, meta_data = Thread.objects.run_advanced_search(request_user=self.user, search_state=ss) + self.assertEqual(1, len(qs)) + self.assertEqual(self.q4.thread_id, qs[0].id) + + def test_run_adv_search_url_author(self): + ss = SearchState(scope=None, sort=None, query=None, tags=None, author=self.user.id, page=None, user_logged_in=None) + qs, meta_data = Thread.objects.run_advanced_search(request_user=self.user, search_state=ss) + self.assertEqual(2, len(qs)) + self.assertEqual(self.q1.thread_id, min(qs[0].id, qs[1].id)) + self.assertEqual(self.q2.thread_id, max(qs[0].id, qs[1].id)) + + ss = SearchState(scope=None, sort=None, query=None, tags=None, author=self.user2.id, page=None, user_logged_in=None) + qs, meta_data = Thread.objects.run_advanced_search(request_user=self.user, search_state=ss) + self.assertEqual(1, len(qs)) + self.assertEqual(self.q3.thread_id, qs[0].id) + + ss = SearchState(scope=None, sort=None, query=None, tags=None, author=self.user3.id, page=None, user_logged_in=None) + qs, meta_data = Thread.objects.run_advanced_search(request_user=self.user, search_state=ss) + self.assertEqual(1, len(qs)) + self.assertEqual(self.q4.thread_id, qs[0].id) + + def test_thread_caching_1(self): + ss = SearchState.get_empty() + qs, meta_data = Thread.objects.run_advanced_search(request_user=self.user, search_state=ss) + qs = list(qs) + + for thread in qs: + self.assertIsNone(getattr(thread, '_question_cache', None)) + self.assertIsNone(getattr(thread, '_last_activity_by_cache', None)) + + post = Post.objects.get(post_type='question', thread=thread.id) + self.assertEqual(post, thread._question_post()) + self.assertEqual(post, thread._question_cache) + self.assertTrue(thread._question_post() is thread._question_cache) + + def test_thread_caching_2_precache_view_data_hack(self): + ss = SearchState.get_empty() + qs, meta_data = Thread.objects.run_advanced_search(request_user=self.user, search_state=ss) + qs = list(qs) + + Thread.objects.precache_view_data_hack(threads=qs) + + for thread in qs: + post = Post.objects.get(post_type='question', thread=thread.id) + self.assertEqual(post.id, thread._question_cache.id) # Cannot compare models instances with deferred model instances + self.assertEqual(post.id, thread._question_post().id) + self.assertTrue(thread._question_post() is thread._question_cache) + + user = User.objects.get(id=thread.last_activity_by_id) + self.assertEqual(user.id, thread._last_activity_by_cache.id) + self.assertTrue(thread.last_activity_by is thread._last_activity_by_cache) + + diff --git a/askbot/tests/search_state_tests.py b/askbot/tests/search_state_tests.py index 791ee206..aca989fc 100644 --- a/askbot/tests/search_state_tests.py +++ b/askbot/tests/search_state_tests.py @@ -1,6 +1,7 @@ from askbot.tests.utils import AskbotTestCase from askbot.search.state_manager import SearchState import askbot.conf +from django.core import urlresolvers class SearchStateTests(AskbotTestCase): @@ -56,6 +57,10 @@ class SearchStateTests(AskbotTestCase): 'scope:unanswered/sort:age-desc/query:alfa/tags:miki,mini/author:12/page:2/', ss.query_string() ) + self.assertEqual( + 'scope:unanswered/sort:age-desc/query:alfa/tags:miki,mini/author:12/page:2/', + ss.deepcopy().query_string() + ) def test_edge_cases_1(self): ss = SearchState( @@ -72,6 +77,10 @@ class SearchStateTests(AskbotTestCase): 'scope:all/sort:age-desc/query:alfa/tags:miki,mini/author:12/page:2/', ss.query_string() ) + self.assertEqual( + 'scope:all/sort:age-desc/query:alfa/tags:miki,mini/author:12/page:2/', + ss.deepcopy().query_string() + ) ss = SearchState( scope='favorite', @@ -86,7 +95,10 @@ class SearchStateTests(AskbotTestCase): self.assertEqual( 'scope:favorite/sort:age-desc/query:alfa/tags:miki,mini/author:12/page:2/', ss.query_string() - + ) + self.assertEqual( + 'scope:favorite/sort:age-desc/query:alfa/tags:miki,mini/author:12/page:2/', + ss.deepcopy().query_string() ) def test_edge_cases_2(self): @@ -144,10 +156,10 @@ class SearchStateTests(AskbotTestCase): def test_query_escaping(self): ss = self._ss(query=' alfa miki maki +-%#?= lalala/: ') # query coming from URL is already unescaped - self.assertEqual( - 'scope:all/sort:activity-desc/query:alfa%20miki%20maki%20+-%25%23%3F%3D%20lalala%2F%3A/page:1/', - ss.query_string() - ) + + qs = 'scope:all/sort:activity-desc/query:alfa%20miki%20maki%20+-%25%23%3F%3D%20lalala%2F%3A/page:1/' + self.assertEqual(qs, ss.query_string()) + self.assertEqual(qs, ss.deepcopy().query_string()) def test_tag_escaping(self): ss = self._ss(tags=' aA09_+.-#, miki ') # tag string coming from URL is already unescaped @@ -158,11 +170,15 @@ class SearchStateTests(AskbotTestCase): def test_extract_users(self): ss = self._ss(query='"@anna haha @"maria fernanda" @\'diego maradona\' hehe [user:karl marx] hoho user:\' george bush \'') - self.assertEquals( + self.assertEqual( sorted(ss.query_users), sorted(['anna', 'maria fernanda', 'diego maradona', 'karl marx', 'george bush']) ) - self.assertEquals(ss.stripped_query, '" haha hehe hoho') + self.assertEqual(sorted(ss.query_users), sorted(ss.deepcopy().query_users)) + + self.assertEqual(ss.stripped_query, '" haha hehe hoho') + self.assertEqual(ss.stripped_query, ss.deepcopy().stripped_query) + self.assertEqual( 'scope:all/sort:activity-desc/query:%22%40anna%20haha%20%40%22maria%20fernanda%22%20%40%27diego%20maradona%27%20hehe%20%5Buser%3Akarl%20%20marx%5D%20hoho%20%20user%3A%27%20george%20bush%20%20%27/page:1/', ss.query_string() @@ -170,24 +186,92 @@ class SearchStateTests(AskbotTestCase): def test_extract_tags(self): ss = self._ss(query='#tag1 [tag: tag2] some text [tag3] query') - self.assertEquals(set(ss.query_tags), set(['tag1', 'tag2', 'tag3'])) - self.assertEquals(ss.stripped_query, 'some text query') + self.assertEqual(set(ss.query_tags), set(['tag1', 'tag2', 'tag3'])) + self.assertEqual(ss.stripped_query, 'some text query') + + self.assertFalse(ss.deepcopy().query_tags is ss.query_tags) + self.assertEqual(set(ss.deepcopy().query_tags), set(ss.query_tags)) + self.assertTrue(ss.deepcopy().stripped_query is ss.stripped_query) + self.assertEqual(ss.deepcopy().stripped_query, ss.stripped_query) def test_extract_title1(self): ss = self._ss(query='some text query [title: what is this?]') - self.assertEquals(ss.query_title, 'what is this?') - self.assertEquals(ss.stripped_query, 'some text query') + self.assertEqual(ss.query_title, 'what is this?') + self.assertEqual(ss.stripped_query, 'some text query') def test_extract_title2(self): ss = self._ss(query='some text query title:"what is this?"') - self.assertEquals(ss.query_title, 'what is this?') - self.assertEquals(ss.stripped_query, 'some text query') + self.assertEqual(ss.query_title, 'what is this?') + self.assertEqual(ss.stripped_query, 'some text query') def test_extract_title3(self): ss = self._ss(query='some text query title:\'what is this?\'') - self.assertEquals(ss.query_title, 'what is this?') - self.assertEquals(ss.stripped_query, 'some text query') + self.assertEqual(ss.query_title, 'what is this?') + self.assertEqual(ss.stripped_query, 'some text query') + + def test_deep_copy_1(self): + # deepcopy() is tested in other tests as well, but this is a dedicated test + # just to make sure in one place that everything is ok: + # 1. immutable properties (strings, ints) are just assigned to the copy + # 2. lists are cloned so that change in the copy doesn't affect the original + + ss = SearchState( + scope='unanswered', + sort='votes-desc', + query='hejho #tag1 [tag: tag2] @user @user2 title:"what is this?"', + tags='miki, mini', + author='12', + page='2', + + user_logged_in=False + ) + ss2 = ss.deepcopy() + + self.assertEqual(ss.scope, 'unanswered') + self.assertTrue(ss.scope is ss2.scope) + + self.assertEqual(ss.sort, 'votes-desc') + self.assertTrue(ss.sort is ss2.sort) + + self.assertEqual(ss.query, 'hejho #tag1 [tag: tag2] @user @user2 title:"what is this?"') + self.assertTrue(ss.query is ss2.query) + + self.assertFalse(ss.tags is ss2.tags) + self.assertItemsEqual(ss.tags, ss2.tags) + + self.assertEqual(ss.author, 12) + self.assertTrue(ss.author is ss2.author) + + self.assertEqual(ss.page, 2) + self.assertTrue(ss.page is ss2.page) + + self.assertEqual(ss.stripped_query, 'hejho') + self.assertTrue(ss.stripped_query is ss2.stripped_query) + + self.assertItemsEqual(ss.query_tags, ['tag1', 'tag2']) + self.assertFalse(ss.query_tags is ss2.query_tags) + + self.assertItemsEqual(ss.query_users, ['user', 'user2']) + self.assertFalse(ss.query_users is ss2.query_users) + + self.assertEqual(ss.query_title, 'what is this?') + self.assertTrue(ss.query_title is ss2.query_title) + + self.assertEqual(ss._questions_url, urlresolvers.reverse('questions')) + self.assertTrue(ss._questions_url is ss2._questions_url) + + def test_deep_copy_2(self): + # Regression test: a special case of deepcopy() when `tags` list is empty, + # there was a bug before where this empty list in original and copy pointed + # to the same list object + ss = SearchState.get_empty() + ss2 = ss.deepcopy() + + self.assertFalse(ss.tags is ss2.tags) + self.assertItemsEqual(ss.tags, ss2.tags) + self.assertItemsEqual([], ss2.tags) + + def test_cannot_add_already_added_tag(self): + ss = SearchState.get_empty().add_tag('double').add_tag('double') + self.assertListEqual(['double'], ss.tags) - def test_negative_match(self): - ss = self._ss(query='some query text') - self.assertEquals(ss.stripped_query, 'some query text') diff --git a/askbot/views/readers.py b/askbot/views/readers.py index 1d592ab6..88d2bf7d 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -10,10 +10,13 @@ import datetime import logging import urllib import operator +from django.contrib.auth.models import User from django.shortcuts import get_object_or_404 from django.http import HttpResponseRedirect, HttpResponse, Http404, HttpResponseNotAllowed from django.core.paginator import Paginator, EmptyPage, InvalidPage from django.template import Context +from django.template.base import Template +from django.template.context import RequestContext from django.utils import simplejson from django.utils.translation import ugettext as _ from django.utils.translation import ungettext @@ -31,6 +34,7 @@ from askbot.forms import AnswerForm, ShowQuestionForm from askbot import models from askbot import schedules from askbot.models.badges import award_badges_signal +from askbot.models.tag import Tag from askbot import const from askbot.utils import functions from askbot.utils.decorators import anonymous_forbidden, ajax_only, get_only @@ -72,24 +76,27 @@ def questions(request, **kwargs): return HttpResponseNotAllowed(['GET']) search_state = SearchState(user_logged_in=request.user.is_authenticated(), **kwargs) - - ####### - page_size = int(askbot_settings.DEFAULT_QUESTIONS_PAGE_SIZE) - qs, meta_data, related_tags = models.Thread.objects.run_advanced_search(request_user=request.user, search_state=search_state, page_size=page_size) - - tag_list_type = askbot_settings.TAG_LIST_FORMAT - if tag_list_type == 'cloud': #force cloud to sort by name - related_tags = sorted(related_tags, key = operator.attrgetter('name')) + qs, meta_data = models.Thread.objects.run_advanced_search(request_user=request.user, search_state=search_state) paginator = Paginator(qs, page_size) if paginator.num_pages < search_state.page: search_state.page = 1 page = paginator.page(search_state.page) - contributors_threads = models.Thread.objects.filter(id__in=[post.thread_id for post in page.object_list]) - contributors = models.Thread.objects.get_thread_contributors(contributors_threads) + page.object_list = list(page.object_list) # evaluate queryset + + # INFO: Because for the time being we need question posts and thread authors + # down the pipeline, we have to precache them in thread objects + models.Thread.objects.precache_view_data_hack(threads=page.object_list) + + related_tags = Tag.objects.get_related_to_search(threads=page.object_list, ignored_tag_names=meta_data.get('ignored_tag_names', [])) + tag_list_type = askbot_settings.TAG_LIST_FORMAT + if tag_list_type == 'cloud': #force cloud to sort by name + related_tags = sorted(related_tags, key = operator.attrgetter('name')) + + contributors = list(models.Thread.objects.get_thread_contributors(thread_list=page.object_list).only('id', 'username', 'gravatar')) paginator_context = { 'is_paginated' : (paginator.count > page_size), @@ -101,8 +108,8 @@ def questions(request, **kwargs): 'previous': page.previous_page_number(), 'next': page.next_page_number(), - 'base_url' : search_state.query_string(),#todo in T sort=>sort_method - 'page_size' : page_size,#todo in T pagesize -> page_size + 'base_url' : search_state.query_string(), + 'page_size' : page_size, } # We need to pass the rss feed url based @@ -145,7 +152,7 @@ def questions(request, **kwargs): questions_tpl = get_template('main_page/questions_loop.html', request) questions_html = questions_tpl.render(Context({ - 'questions': page, + 'threads': page, 'search_state': search_state, 'reset_method_count': reset_method_count, })) @@ -158,7 +165,6 @@ def questions(request, **kwargs): }, 'paginator': paginator_html, 'question_counter': question_counter, - 'questions': list(), 'faces': [extra_tags.gravatar(contributor, 48) for contributor in contributors], 'feed_url': context_feed_url, 'query_string': search_state.query_string(), @@ -187,7 +193,7 @@ def questions(request, **kwargs): 'page_class': 'main-page', 'page_size': page_size, 'query': search_state.query, - 'questions' : page, + 'threads' : page, 'questions_count' : paginator.count, 'reset_method_count': reset_method_count, 'scope': search_state.scope, -- cgit v1.2.3-1-g7c22 From ec95dff2c17de24448f4eb29fbc6907991fbc800 Mon Sep 17 00:00:00 2001 From: Tomasz Zielinski Date: Sun, 22 Jan 2012 17:55:38 +0100 Subject: Fixes for failing tests (for both PostgreSQL and MySQL) --- askbot/management/commands/merge_users.py | 8 +++++ .../0106_update_postgres_full_text_setup.py | 4 +++ .../thread_and_post_models_01162012.plsql | 1 + askbot/tests/page_load_tests.py | 36 ++++++++++++++++------ askbot/tests/post_model_tests.py | 14 ++++----- 5 files changed, 46 insertions(+), 17 deletions(-) diff --git a/askbot/management/commands/merge_users.py b/askbot/management/commands/merge_users.py index 3c7069e5..9eb76756 100644 --- a/askbot/management/commands/merge_users.py +++ b/askbot/management/commands/merge_users.py @@ -1,6 +1,14 @@ from django.core.management.base import CommandError, BaseCommand from askbot.models import User +# TODO: this command is broken - doesn't take into account UNIQUE constraints +# and therefore causes db errors: +# In SQLite: "Warning: columns feed_type, subscriber_id are not unique" +# In MySQL: "Warning: (1062, "Duplicate entry 'm_and_c-2' for key 'askbot_emailfeedsetting_feed_type_6da6fdcd_uniq'")" +# In PostgreSQL: "Warning: duplicate key value violates unique constraint "askbot_emailfeedsetting_feed_type_6da6fdcd_uniq" +# "DETAIL: Key (feed_type, subscriber_id)=(m_and_c, 619) already exists." +# (followed by series of "current transaction is aborted, commands ignored until end of transaction block" warnings) + class MergeUsersBaseCommand(BaseCommand): args = ' ' help = 'Merge an account and all information from a to a , deleting the ' diff --git a/askbot/migrations/0106_update_postgres_full_text_setup.py b/askbot/migrations/0106_update_postgres_full_text_setup.py index 0f940b96..b0b0c668 100644 --- a/askbot/migrations/0106_update_postgres_full_text_setup.py +++ b/askbot/migrations/0106_update_postgres_full_text_setup.py @@ -1,4 +1,5 @@ # encoding: utf-8 +import sys import askbot from askbot.search.postgresql import setup_full_text_search import datetime @@ -14,6 +15,9 @@ class Migration(DataMigration): def forwards(self, orm): "Write your forwards methods here." + if 'test' in sys.argv: + return # Somehow this migration fails when called from test runner + if 'postgresql_psycopg2' in askbot.get_database_engine_name(): script_path = os.path.join( askbot.get_install_directory(), diff --git a/askbot/search/postgresql/thread_and_post_models_01162012.plsql b/askbot/search/postgresql/thread_and_post_models_01162012.plsql index 7156833b..44d0ea4a 100644 --- a/askbot/search/postgresql/thread_and_post_models_01162012.plsql +++ b/askbot/search/postgresql/thread_and_post_models_01162012.plsql @@ -219,4 +219,5 @@ DROP TRIGGER IF EXISTS post_search_vector_update_trigger on askbot_post; CREATE TRIGGER post_search_vector_update_trigger BEFORE INSERT OR UPDATE ON askbot_post FOR EACH ROW EXECUTE PROCEDURE post_trigger(); +DROP INDEX IF EXISTS askbot_search_idx; CREATE INDEX askbot_search_idx ON askbot_thread USING gin(text_search_vector); diff --git a/askbot/tests/page_load_tests.py b/askbot/tests/page_load_tests.py index f9385bec..ace41e8b 100644 --- a/askbot/tests/page_load_tests.py +++ b/askbot/tests/page_load_tests.py @@ -1,19 +1,19 @@ from askbot.search.state_manager import SearchState -from django.test import TestCase from django.test import signals -from django.template import defaultfilters from django.conf import settings from django.core.urlresolvers import reverse +from django.core import management + import coffin import coffin.template + from askbot import models from askbot.utils.slug import slugify from askbot.deployment import package_utils from askbot.tests.utils import AskbotTestCase from askbot.conf import settings as askbot_settings from askbot.tests.utils import skipIf -import sys -import os + def patch_jinja2(): @@ -37,16 +37,32 @@ if CMAJOR == 0 and CMINOR == 3 and CMICRO < 4: class PageLoadTestCase(AskbotTestCase): - def _fixture_setup(self): - from django.core import management + ############################################# + # + # INFO: We load test data once for all tests in this class (setUpClass + cleanup in tearDownClass) + # + # We also disable (by overriding _fixture_setup/teardown) per-test fixture setup, + # which by default flushes the database for non-transactional db engines like MySQL+MyISAM. + # For transactional engines it only messes with transactions, but to keep things uniform + # for both types of databases we disable it all. + # + @classmethod + def setUpClass(cls): + management.call_command('flush', verbosity=0, interactive=False) management.call_command('askbot_add_test_content', verbosity=0, interactive=False) - super(PageLoadTestCase, self)._fixture_setup() - def _fixture_teardown(self): - super(PageLoadTestCase, self)._fixture_teardown() - from django.core import management + @classmethod + def tearDownClass(self): management.call_command('flush', verbosity=0, interactive=False) + def _fixture_setup(self): + pass + + def _fixture_teardown(self): + pass + + ############################################# + def try_url( self, url_name, status_code=200, template=None, diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py index 7e66d144..824a4a8f 100644 --- a/askbot/tests/post_model_tests.py +++ b/askbot/tests/post_model_tests.py @@ -114,9 +114,9 @@ class PostModelTests(AskbotTestCase): self.user = self.u1 q = self.post_question() - c1 = self.post_comment(parent_post=q) - c2 = q.add_comment(user=self.user, comment='blah blah') - c3 = self.post_comment(parent_post=q) + c1 = self.post_comment(parent_post=q, timestamp=datetime.datetime(2010, 10, 2, 14, 33, 20)) + c2 = q.add_comment(user=self.user, comment='blah blah', added_at=datetime.datetime(2010, 10, 2, 14, 33, 21)) + c3 = self.post_comment(parent_post=q, body_text='blah blah 2', timestamp=datetime.datetime(2010, 10, 2, 14, 33, 22)) Post.objects.precache_comments(for_posts=[q], visitor=self.user) self.assertListEqual([c1, c2, c3], q._cached_comments) @@ -138,9 +138,9 @@ class PostModelTests(AskbotTestCase): self.user = self.u1 q = self.post_question() - c1 = self.post_comment(parent_post=q) - c2 = q.add_comment(user=self.user, comment='blah blah') - c3 = self.post_comment(parent_post=q) + c1 = self.post_comment(parent_post=q, timestamp=datetime.datetime(2010, 10, 2, 14, 33, 20)) + c2 = q.add_comment(user=self.user, comment='blah blah', added_at=datetime.datetime(2010, 10, 2, 14, 33, 21)) + c3 = self.post_comment(parent_post=q, timestamp=datetime.datetime(2010, 10, 2, 14, 33, 22)) Post.objects.precache_comments(for_posts=[q], visitor=self.user) self.assertListEqual([c1, c2, c3], q._cached_comments) @@ -209,7 +209,7 @@ class ThreadTagModelsTests(AskbotTestCase): tags = Tag.objects.get_related_to_search(threads=[self.q3.thread], ignored_tag_names=['tag6']) self.assertListEqual([], [t.name for t in tags]) - tags = Tag.objects.get_related_to_search(threads=[self.q1.thread, self.q2.thread, self.q4], ignored_tag_names=['tag2']) + tags = Tag.objects.get_related_to_search(threads=[self.q1.thread, self.q2.thread, self.q4.thread], ignored_tag_names=['tag2']) self.assertListEqual(['tag3', 'tag1', 'tag4', 'tag5', 'tag6'], [t.name for t in tags]) self.assertListEqual([3, 2, 2, 2, 1], [t.local_used_count for t in tags]) self.assertListEqual([3, 2, 2, 2, 2], [t.used_count for t in tags]) -- cgit v1.2.3-1-g7c22 From dae7f5592f1d7102501f2b9027b7a99a222613da Mon Sep 17 00:00:00 2001 From: Jordi Bofill Date: Mon, 23 Jan 2012 14:14:46 +0100 Subject: updated fuzzy entries in catalan tranlation --- askbot/locale/ca/LC_MESSAGES/django.mo | Bin 88690 -> 90367 bytes askbot/locale/ca/LC_MESSAGES/django.po | 58 ++++++++++++++------------------- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/askbot/locale/ca/LC_MESSAGES/django.mo b/askbot/locale/ca/LC_MESSAGES/django.mo index 7904842d..1be5bb52 100644 Binary files a/askbot/locale/ca/LC_MESSAGES/django.mo and b/askbot/locale/ca/LC_MESSAGES/django.mo differ diff --git a/askbot/locale/ca/LC_MESSAGES/django.po b/askbot/locale/ca/LC_MESSAGES/django.po index 7f01961f..f65be5f3 100644 --- a/askbot/locale/ca/LC_MESSAGES/django.po +++ b/askbot/locale/ca/LC_MESSAGES/django.po @@ -58,11 +58,11 @@ msgid "please enter a descriptive title for your question" msgstr "Escriviu un títol descriptiu de la pregunta" #: forms.py:111 -#, fuzzy, python-format +#, python-format msgid "title must be > %d character" msgid_plural "title must be > %d characters" -msgstr[0] "el títol ha de tenir més de 10 caràcters" -msgstr[1] "el títol ha de tenir més de 10 caràcters" +msgstr[0] "el títol ha de tenir més d'%d caràcter" +msgstr[1] "el títol ha de tenir més de %d caràcters" #: forms.py:131 msgid "content" @@ -75,7 +75,7 @@ msgid "tags" msgstr "etiquetes" #: forms.py:168 -#, fuzzy, python-format +#, python-format msgid "" "Tags are short keywords, with no spaces within. Up to %(max_tags)d tag can " "be used." @@ -83,10 +83,10 @@ msgid_plural "" "Tags are short keywords, with no spaces within. Up to %(max_tags)d tags can " "be used." msgstr[0] "" -"Les etiquetes són paraules clau curtes, sense espais. Es poden usar fins a 5 " -"etiquetes." +"Les etiquetes són paraules clau curtes, sense espais. Es poden usar fins a %(max_tags)d " +"etiqueta." msgstr[1] "" -"Les etiquetes són paraules clau curtes, sense espais. Es poden usar fins a 5 " +"Les etiquetes són paraules clau curtes, sense espais. Es poden usar fins a %(max_tags)d " "etiquetes." #: forms.py:201 skins/default/templates/question_retag.html:58 @@ -3106,11 +3106,11 @@ msgid "suspended users cannot remove flags" msgstr "els usuaris deshabilitats no poden treure senyals" #: models/__init__.py:809 -#, fuzzy, python-format +#, python-format msgid "need > %(min_rep)d point to remove flag" msgid_plural "need > %(min_rep)d points to remove flag" -msgstr[0] "s'han de tenir més de %(min_rep)s punts per poder treure senyals" -msgstr[1] "s'han de tenir més de %(min_rep)s punts per poder treure senyals" +msgstr[0] "s'han de tenir més d'%(min_rep)d punt per poder treure senyals" +msgstr[1] "s'han de tenir més de %(min_rep)d punts per poder treure senyals" #: models/__init__.py:828 msgid "you don't have the permission to remove all flags" @@ -3689,7 +3689,6 @@ msgid "No email" msgstr "Cap correu electrònic" #: skins/common/templates/authopenid/authopenid_macros.html:53 -#, fuzzy msgid "Please enter your user name, then sign in" msgstr "Introduïu el vostre nom d'usuari i contrasenya per entrar" @@ -4230,9 +4229,8 @@ msgstr "editar" #: skins/common/templates/question/answer_controls.html:16 #: skins/common/templates/question/question_controls.html:23 #: skins/common/templates/question/question_controls.html:24 -#, fuzzy msgid "remove all flags" -msgstr "treure senyal" +msgstr "treure senyals" #: skins/common/templates/question/answer_controls.html:22 #: skins/common/templates/question/answer_controls.html:32 @@ -4454,9 +4452,8 @@ msgstr "Guardar edició" #: skins/default/templates/ask.html:52 #: skins/default/templates/question_edit.html:76 #: skins/default/templates/question/javascript.html:92 -#, fuzzy msgid "show preview" -msgstr "ocultar previsualització" +msgstr "mostrar previsualització" #: skins/default/templates/ask.html:4 msgid "Ask a question" @@ -4465,9 +4462,9 @@ msgstr "Feu una pregunta" #: skins/default/templates/badge.html:5 skins/default/templates/badge.html:9 #: skins/default/templates/user_profile/user_recent.html:16 #: skins/default/templates/user_profile/user_stats.html:110 -#, fuzzy, python-format +#, python-format msgid "%(name)s" -msgstr "Insígnia \"%(name)s\"" +msgstr "" #: skins/default/templates/badge.html:5 msgid "Badge" @@ -4481,9 +4478,9 @@ msgstr "Insígnia \"%(name)s\"" #: skins/default/templates/badge.html:9 #: skins/default/templates/user_profile/user_recent.html:16 #: skins/default/templates/user_profile/user_stats.html:108 -#, fuzzy, python-format +#, python-format msgid "%(description)s" -msgstr "subscripcions" +msgstr "" #: skins/default/templates/badge.html:14 msgid "user received this badge:" @@ -4594,9 +4591,8 @@ msgstr "" "s'ha contestat abans." #: skins/default/templates/faq_static.html:10 -#, fuzzy msgid "What questions should I avoid asking?" -msgstr "Que he d'evitar en les meves respostes?" +msgstr "Quines qüestions he d'evitar preguntar?" #: skins/default/templates/faq_static.html:11 msgid "" @@ -5113,9 +5109,8 @@ msgstr[0] "teniu una nova resposta" msgstr[1] "teniu %(response_count)s noves respostes" #: skins/default/templates/macros.html:635 -#, fuzzy msgid "no new responses yet" -msgstr "teniu una nova resposta" +msgstr "no hi ha respostes" #: skins/default/templates/macros.html:650 #: skins/default/templates/macros.html:651 @@ -5325,9 +5320,8 @@ msgid "with %(author_name)s's contributions" msgstr "amb contribució de %(author_name)s" #: skins/default/templates/main_page/headline.html:12 -#, fuzzy msgid "Tagged" -msgstr "reetiquetat" +msgstr "Reetiquetat" #: skins/default/templates/main_page/headline.html:23 msgid "Search tips:" @@ -5782,9 +5776,8 @@ msgid "age" msgstr "edat" #: skins/default/templates/user_profile/user_info.html:83 -#, fuzzy msgid "age unit" -msgstr "Missatge enviat" +msgstr "unitat d'edat" #: skins/default/templates/user_profile/user_info.html:88 msgid "todays unused votes" @@ -5873,9 +5866,8 @@ msgid "'Approved' status means the same as regular user." msgstr "" #: skins/default/templates/user_profile/user_moderate.html:83 -#, fuzzy msgid "Suspended users can only edit or delete their own posts." -msgstr "els usuaris deshabilitats no poden senyalar entrades" +msgstr "Els usuaris deshabilitats només poden editar o esborrar les seves entrades" #: skins/default/templates/user_profile/user_moderate.html:86 msgid "" @@ -5929,9 +5921,8 @@ msgid "karma" msgstr "reputació" #: skins/default/templates/user_profile/user_reputation.html:11 -#, fuzzy msgid "Your karma change log." -msgstr "registre de modificacions de reputació de %(user_name)s" +msgstr "Registre de canvis en la vostre reputació." #: skins/default/templates/user_profile/user_reputation.html:13 #, python-format @@ -6253,9 +6244,8 @@ msgid "UNANSWERED" msgstr "SENSE RESPONDRE" #: skins/default/templates/widgets/scope_nav.html:8 -#, fuzzy msgid "see your followed questions" -msgstr "preguntes seguides" +msgstr "mostrar les preguntes seguides" #: skins/default/templates/widgets/scope_nav.html:8 msgid "FOLLOWED" @@ -6287,7 +6277,7 @@ msgstr "configuració" #: templatetags/extra_filters.py:145 templatetags/extra_filters_jinja.py:264 msgid "no items in counter" -msgstr "" +msgstr "no" #: utils/decorators.py:90 views/commands.py:113 views/commands.py:133 msgid "Oops, apologies - there was some error" -- cgit v1.2.3-1-g7c22 From d1056fc8abcb09720704b4c4980818ccf5184534 Mon Sep 17 00:00:00 2001 From: Tomasz Zielinski Date: Tue, 24 Jan 2012 18:15:21 +0100 Subject: Main page thread summary caching BETA --- askbot/const/__init__.py | 3 +- askbot/models/post.py | 2 +- askbot/models/question.py | 71 +++- askbot/search/state_manager.py | 8 + .../templates/main_page/questions_loop.html | 3 +- askbot/tests/page_load_tests.py | 3 +- askbot/tests/post_model_tests.py | 368 +++++++++++++++++++++ askbot/views/commands.py | 10 + askbot/views/readers.py | 5 +- 9 files changed, 462 insertions(+), 11 deletions(-) diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py index 56ef89cf..0f981dee 100644 --- a/askbot/const/__init__.py +++ b/askbot/const/__init__.py @@ -84,7 +84,8 @@ UNANSWERED_QUESTION_MEANING_CHOICES = ( #correct regexes - plus this must be an anchored regex #to do full string match TAG_CHARS = r'\w+.#-' -TAG_REGEX = r'^[%s]+$' % TAG_CHARS +TAG_REGEX_BARE = r'[%s]+' % TAG_CHARS +TAG_REGEX = r'^%s$' % TAG_REGEX_BARE TAG_SPLIT_REGEX = r'[ ,]+' TAG_SEP = ',' # has to be valid TAG_SPLIT_REGEX char and MUST NOT be in const.TAG_CHARS EMAIL_REGEX = re.compile(r'\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b', re.I) diff --git a/askbot/models/post.py b/askbot/models/post.py index 6cd9f7eb..5cb9708f 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -174,9 +174,9 @@ class PostManager(BaseQuerySetManager): ) #update thread data - thread.set_last_activity(last_activity_at=added_at, last_activity_by=author) 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 #set notification/delete if email_notify: diff --git a/askbot/models/question.py b/askbot/models/question.py index a112830d..cd1ca184 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -1,11 +1,12 @@ import datetime import operator -from askbot.utils import mysql +import re from django.conf import settings from django.db import models from django.contrib.auth.models import User from django.utils.translation import ugettext as _ +from django.core import cache # import cache, not from cache import cache, to be able to monkey-patch cache.cache in test cases import askbot import askbot.conf @@ -16,6 +17,9 @@ from askbot.models import signals from askbot import const from askbot.utils.lists import LazyList from askbot.utils import mysql +from askbot.skins.loaders import get_template #jinja2 template loading enviroment +from askbot.search.state_manager import DummySearchState + class ThreadManager(models.Manager): def get_tag_summary_from_threads(self, threads): @@ -246,6 +250,13 @@ class ThreadManager(models.Manager): return qs, meta_data def precache_view_data_hack(self, threads): + # TODO: Re-enable this when we have a good test cases to verify that it works properly. + # + # E.g.: - make sure that not precaching give threads never increase # of db queries for the main page + # - make sure that it really works, i.e. stuff for non-cached threads is fetched properly + # Precache data only for non-cached threads - only those will be rendered + #threads = [thread for thread in threads if not thread.summary_html_cached()] + page_questions = Post.objects.filter(post_type='question', thread__in=[obj.id for obj in threads])\ .only('id', 'thread', 'score', 'is_anonymous', 'summary', 'post_type', 'deleted') # pick only the used fields page_question_map = {} @@ -281,6 +292,8 @@ class ThreadManager(models.Manager): class Thread(models.Model): + SUMMARY_CACHE_KEY_TPL = 'thread-question-summary-%d' + title = models.CharField(max_length=300) tags = models.ManyToManyField('Tag', related_name='threads') @@ -313,7 +326,9 @@ class Thread(models.Model): class Meta: app_label = 'askbot' - def _question_post(self): + def _question_post(self, refresh=False): + if refresh and hasattr(self, '_question_cache'): + delattr(self, '_question_cache') post = getattr(self, '_question_cache', None) if post: return post @@ -335,6 +350,9 @@ class Thread(models.Model): qset = Thread.objects.filter(id=self.id) qset.update(view_count=models.F('view_count') + increment) self.view_count = qset.values('view_count')[0]['view_count'] # get the new view_count back because other pieces of code relies on such behaviour + #################################################################### + self.update_summary_html() # regenerate question/thread summary html + #################################################################### def set_closed_status(self, closed, closed_by, closed_at, close_reason): self.closed = closed @@ -354,6 +372,9 @@ class Thread(models.Model): self.last_activity_at = last_activity_at self.last_activity_by = last_activity_by self.save() + #################################################################### + self.update_summary_html() # regenerate question/thread summary html + #################################################################### def get_tag_names(self): "Creates a list of Tag names from the ``tagnames`` attribute." @@ -529,6 +550,10 @@ class Thread(models.Model): self.tags.add(*added_tags) modified_tags.extend(added_tags) + #################################################################### + self.update_summary_html() # regenerate question/thread summary html + #################################################################### + #if there are any modified tags, update their use counts if modified_tags: Tag.objects.update_use_counts(modified_tags) @@ -593,7 +618,47 @@ class Thread(models.Model): return last_updated_at, last_updated_by - + def get_summary_html(self, search_state): + html = self.get_cached_summary_html() + if not html: + html = self.update_summary_html() + + # use `<<<` and `>>>` because they cannot be confused with user input + # - if user accidentialy types <<>> into question title or body, + # then in html it'll become escaped like this: <<<tag-name>>> + regex = re.compile(r'<<<(%s)>>>' % const.TAG_REGEX_BARE) + + while True: + match = regex.search(html) + if not match: + break + seq = match.group(0) # e.g "<<>>" + tag = match.group(1) # e.g "my-tag" + full_url = search_state.add_tag(tag).full_url() + html = html.replace(seq, full_url) + + return html + + def get_cached_summary_html(self): + return cache.cache.get(self.SUMMARY_CACHE_KEY_TPL % self.id) + + def update_summary_html(self): + context = { + 'thread': self, + 'question': self._question_post(refresh=True), # fetch new question post to make sure we're up-to-date + 'search_state': DummySearchState(), + } + html = get_template('widgets/question_summary.html').render(context) + # INFO: Timeout is set to 30 days: + # * timeout=0/None is not a reliable cross-backend way to set infinite timeout + # * We probably don't need to pollute the cache with threads older than 30 days + # * Additionally, Memcached treats timeouts > 30day as dates (https://code.djangoproject.com/browser/django/tags/releases/1.3/django/core/cache/backends/memcached.py#L36), + # which probably doesn't break anything but if we can stick to 30 days then let's stick to it + cache.cache.set(self.SUMMARY_CACHE_KEY_TPL % self.id, html, timeout=60*60*24*30) + return html + + def summary_html_cached(self): + return cache.cache.has_key(self.SUMMARY_CACHE_KEY_TPL % self.id) #class Question(content.Content): diff --git a/askbot/search/state_manager.py b/askbot/search/state_manager.py index 29e6484e..232d64e9 100644 --- a/askbot/search/state_manager.py +++ b/askbot/search/state_manager.py @@ -232,3 +232,11 @@ class SearchState(object): ss = self.deepcopy() ss.page = new_page return ss + + +class DummySearchState(object): # Used for caching question/thread summaries + def add_tag(self, tag): + self.tag = tag + return self + def full_url(self): + return '<<<%s>>>' % self.tag diff --git a/askbot/skins/default/templates/main_page/questions_loop.html b/askbot/skins/default/templates/main_page/questions_loop.html index 6d83032c..6a5e5e3d 100644 --- a/askbot/skins/default/templates/main_page/questions_loop.html +++ b/askbot/skins/default/templates/main_page/questions_loop.html @@ -1,7 +1,8 @@ {% import "macros.html" as macros %} {# cache 0 "questions" questions search_tags scope sort query context.page language_code #} {% for thread in threads.object_list %} - {{macros.question_summary(thread, thread._question_post(), search_state=search_state)}} + {# {{macros.question_summary(thread, thread._question_post(), search_state=search_state)}} #} + {{ thread.get_summary_html(search_state=search_state) }} {% endfor %} {% if threads.object_list|length == 0 %} {% include "main_page/nothing_found.html" %} diff --git a/askbot/tests/page_load_tests.py b/askbot/tests/page_load_tests.py index 1a56f951..18d8d69c 100644 --- a/askbot/tests/page_load_tests.py +++ b/askbot/tests/page_load_tests.py @@ -108,7 +108,8 @@ class PageLoadTestCase(AskbotTestCase): 'one template') % url ) - self.assertEqual(r.template[0].name, template) + #self.assertEqual(r.template[0].name, template) + self.assertIn(template, [t.name for t in r.template]) else: raise Exception('unexpected error while runnig test') diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py index 824a4a8f..44bcb10a 100644 --- a/askbot/tests/post_model_tests.py +++ b/askbot/tests/post_model_tests.py @@ -1,11 +1,19 @@ +import copy import datetime from operator import attrgetter +import time from askbot.search.state_manager import SearchState +from askbot.skins.loaders import get_template from django.contrib.auth.models import User +from django.core import cache, urlresolvers +from django.core.cache.backends.dummy import DummyCache +from django.core.cache.backends.locmem import LocMemCache from django.core.exceptions import ValidationError from askbot.tests.utils import AskbotTestCase from askbot.models import Post, PostRevision, Thread, Tag +from askbot.search.state_manager import DummySearchState +from django.utils import simplejson class PostModelTests(AskbotTestCase): @@ -300,3 +308,363 @@ class ThreadTagModelsTests(AskbotTestCase): self.assertTrue(thread.last_activity_by is thread._last_activity_by_cache) +class ThreadRenderLowLevelCachingTests(AskbotTestCase): + def setUp(self): + self.create_user() + # INFO: title and body_text should contain tag placeholders so that we can check if they stay untouched + # - only real tag placeholders in tag widget should be replaced with search URLs + self.q = self.post_question(title="<<>> fake title", body_text="<<>> <<>> cheating", tags='tag1 tag2 tag3') + + self.old_cache = cache.cache + + def tearDown(self): + cache.cache = self.old_cache # Restore caching + + def test_thread_summary_rendering_dummy_cache(self): + cache.cache = DummyCache('', {}) # Disable caching + + ss = SearchState.get_empty() + thread = self.q.thread + test_html = thread.get_summary_html(search_state=ss) + + context = { + 'thread': thread, + 'question': thread._question_post(), + 'search_state': ss, + } + proper_html = get_template('widgets/question_summary.html').render(context) + self.assertEqual(test_html, proper_html) + + # Make double-check that all tags are included + self.assertTrue(ss.add_tag('tag1').full_url() in test_html) + self.assertTrue(ss.add_tag('tag2').full_url() in test_html) + self.assertTrue(ss.add_tag('tag3').full_url() in test_html) + self.assertFalse(ss.add_tag('mini-mini').full_url() in test_html) + + # Make sure that title and body text are escaped properly. + # This should be obvious at this point, if the above test passes, but why not be explicit + # UPDATE: And voila, these tests catched double-escaping bug in template, where `<` was `&lt;` + # And indeed, post.summary is escaped before saving, in parse_and_save_post() + # UPDATE 2:Weird things happen with question summary (it's double escaped etc., really weird) so + # let's just make sure that there are no tag placeholders left + self.assertTrue('<<<tag1>>> fake title' in proper_html) + #self.assertTrue('<<<tag2>>> <<<tag3>>> cheating' in proper_html) + self.assertFalse('<<>>' in proper_html) + self.assertFalse('<<>>' in proper_html) + self.assertFalse('<<>>' in proper_html) + + ### + + ss = ss.add_tag('mini-mini') + context['search_state'] = ss + test_html = thread.get_summary_html(search_state=ss) + proper_html = get_template('widgets/question_summary.html').render(context) + + self.assertEqual(test_html, proper_html) + + # Make double-check that all tags are included (along with `mini-mini` tag) + self.assertTrue(ss.add_tag('tag1').full_url() in test_html) + self.assertTrue(ss.add_tag('tag2').full_url() in test_html) + self.assertTrue(ss.add_tag('tag3').full_url() in test_html) + + def test_thread_summary_locmem_cache(self): + cache.cache = LocMemCache('', {}) # Enable local caching + + thread = self.q.thread + key = Thread.SUMMARY_CACHE_KEY_TPL % thread.id + + self.assertTrue(thread.summary_html_cached()) + self.assertIsNotNone(thread.get_cached_summary_html()) + + ### + cache.cache.delete(key) # let's start over + + self.assertFalse(thread.summary_html_cached()) + self.assertIsNone(thread.get_cached_summary_html()) + + context = { + 'thread': thread, + 'question': self.q, + 'search_state': DummySearchState(), + } + html = get_template('widgets/question_summary.html').render(context) + filled_html = html.replace('<<>>', SearchState.get_empty().add_tag('tag1').full_url())\ + .replace('<<>>', SearchState.get_empty().add_tag('tag2').full_url())\ + .replace('<<>>', SearchState.get_empty().add_tag('tag3').full_url()) + + self.assertEqual(filled_html, thread.get_summary_html(search_state=SearchState.get_empty())) + self.assertTrue(thread.summary_html_cached()) + self.assertEqual(html, thread.get_cached_summary_html()) + + ### + cache.cache.set(key, 'Test <<>>', timeout=100) + + self.assertTrue(thread.summary_html_cached()) + self.assertEqual('Test <<>>', thread.get_cached_summary_html()) + self.assertEqual( + 'Test %s' % SearchState.get_empty().add_tag('tag1').full_url(), + thread.get_summary_html(search_state=SearchState.get_empty()) + ) + + ### + cache.cache.set(key, 'TestBBB <<>>', timeout=100) + + self.assertTrue(thread.summary_html_cached()) + self.assertEqual('TestBBB <<>>', thread.get_cached_summary_html()) + self.assertEqual( + 'TestBBB %s' % SearchState.get_empty().add_tag('tag1').full_url(), + thread.get_summary_html(search_state=SearchState.get_empty()) + ) + + ### + cache.cache.delete(key) + thread.update_summary_html = lambda: "Monkey-patched <<>>" + + self.assertFalse(thread.summary_html_cached()) + self.assertIsNone(thread.get_cached_summary_html()) + self.assertEqual( + 'Monkey-patched %s' % SearchState.get_empty().add_tag('tag2').full_url(), + thread.get_summary_html(search_state=SearchState.get_empty()) + ) + + + +class ThreadRenderCacheUpdateTests(AskbotTestCase): + def setUp(self): + self.create_user() + self.user.set_password('pswd') + self.user.save() + assert self.client.login(username=self.user.username, password='pswd') + + self.create_user(username='user2') + self.user2.set_password('pswd') + self.user2.reputation = 10000 + self.user2.save() + + self.old_cache = cache.cache + cache.cache = LocMemCache('', {}) # Enable local caching + + def tearDown(self): + cache.cache = self.old_cache # Restore caching + + def _html_for_question(self, q): + context = { + 'thread': q.thread, + 'question': q, + 'search_state': DummySearchState(), + } + html = get_template('widgets/question_summary.html').render(context) + return html + + def test_post_question(self): + self.assertEqual(0, Post.objects.count()) + response = self.client.post(urlresolvers.reverse('ask'), data={ + 'title': 'test title', + 'text': 'test body text', + 'tags': 'tag1 tag2', + }) + self.assertEqual(1, Post.objects.count()) + question = Post.objects.all()[0] + self.assertRedirects(response=response, expected_url=question.get_absolute_url()) + + self.assertEqual('test title', question.thread.title) + self.assertEqual('test body text', question.text) + self.assertItemsEqual(['tag1', 'tag2'], list(question.thread.tags.values_list('name', flat=True))) + self.assertEqual(0, question.thread.answer_count) + + self.assertTrue(question.thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(question) + self.assertEqual(html, question.thread.get_cached_summary_html()) + + def test_edit_question(self): + self.assertEqual(0, Post.objects.count()) + question = self.post_question() + + thread = Thread.objects.all()[0] + self.assertEqual(0, thread.answer_count) + self.assertEqual(thread.last_activity_at, question.added_at) + self.assertEqual(thread.last_activity_by, question.author) + + time.sleep(1.5) # compensate for 1-sec time resolution in some databases + + response = self.client.post(urlresolvers.reverse('edit_question', kwargs={'id': question.id}), data={ + 'title': 'edited title', + 'text': 'edited body text', + 'tags': 'tag1 tag2', + 'summary': 'just some edit', + }) + self.assertEqual(1, Post.objects.count()) + question = Post.objects.all()[0] + self.assertRedirects(response=response, expected_url=question.get_absolute_url()) + + thread = question.thread + self.assertEqual(0, thread.answer_count) + self.assertTrue(thread.last_activity_at > question.added_at) + self.assertEqual(thread.last_activity_at, question.last_edited_at) + self.assertEqual(thread.last_activity_by, question.author) + + self.assertTrue(question.thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(question) + self.assertEqual(html, question.thread.get_cached_summary_html()) + + def test_retag_question(self): + self.assertEqual(0, Post.objects.count()) + question = self.post_question() + response = self.client.post(urlresolvers.reverse('retag_question', kwargs={'id': question.id}), data={ + 'tags': 'tag1 tag2', + }) + self.assertEqual(1, Post.objects.count()) + question = Post.objects.all()[0] + self.assertRedirects(response=response, expected_url=question.get_absolute_url()) + + self.assertItemsEqual(['tag1', 'tag2'], list(question.thread.tags.values_list('name', flat=True))) + + self.assertTrue(question.thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(question) + self.assertEqual(html, question.thread.get_cached_summary_html()) + + def test_answer_question(self): + self.assertEqual(0, Post.objects.count()) + question = self.post_question() + self.assertEqual(1, Post.objects.count()) + + thread = question.thread + self.assertEqual(0, thread.answer_count) + self.assertEqual(thread.last_activity_at, question.added_at) + self.assertEqual(thread.last_activity_by, question.author) + + self.client.logout() + self.client.login(username='user2', password='pswd') + time.sleep(1.5) # compensate for 1-sec time resolution in some databases + response = self.client.post(urlresolvers.reverse('answer', kwargs={'id': question.id}), data={ + 'text': 'answer longer than 10 chars', + }) + self.assertEqual(2, Post.objects.count()) + answer = Post.objects.get_answers()[0] + self.assertRedirects(response=response, expected_url=answer.get_absolute_url()) + + thread = answer.thread + self.assertEqual(1, thread.answer_count) + self.assertEqual(thread.last_activity_at, answer.added_at) + self.assertEqual(thread.last_activity_by, answer.author) + + self.assertTrue(question.added_at < answer.added_at) + self.assertNotEqual(question.author, answer.author) + + self.assertTrue(thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(thread._question_post()) + self.assertEqual(html, thread.get_cached_summary_html()) + + def test_edit_answer(self): + self.assertEqual(0, Post.objects.count()) + question = self.post_question() + self.assertEqual(question.thread.last_activity_at, question.added_at) + self.assertEqual(question.thread.last_activity_by, question.author) + + time.sleep(1.5) # compensate for 1-sec time resolution in some databases + question_thread = copy.deepcopy(question.thread) # INFO: in the line below question.thread is touched and it reloads its `last_activity_by` field so we preserve it here + answer = self.post_answer(user=self.user2, question=question) + self.assertEqual(2, Post.objects.count()) + + time.sleep(1.5) # compensate for 1-sec time resolution in some databases + self.client.logout() + self.client.login(username='user2', password='pswd') + response = self.client.post(urlresolvers.reverse('edit_answer', kwargs={'id': answer.id}), data={ + 'text': 'edited body text', + 'summary': 'just some edit', + }) + self.assertRedirects(response=response, expected_url=answer.get_absolute_url()) + + answer = Post.objects.get(id=answer.id) + thread = answer.thread + self.assertEqual(thread.last_activity_at, answer.last_edited_at) + self.assertEqual(thread.last_activity_by, answer.last_edited_by) + self.assertTrue(thread.last_activity_at > question_thread.last_activity_at) + self.assertNotEqual(thread.last_activity_by, question_thread.last_activity_by) + + self.assertTrue(thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(thread._question_post()) + self.assertEqual(html, thread.get_cached_summary_html()) + + def test_view_count(self): + question = self.post_question() + self.assertEqual(0, question.thread.view_count) + self.assertEqual(0, Thread.objects.all()[0].view_count) + self.client.logout() + # INFO: We need to pass some headers to make question() view believe we're not a robot + self.client.get( + urlresolvers.reverse('question', kwargs={'id': question.id}), + {}, + follow=True, # the first view redirects to the full question url (with slug in it), so we have to follow that redirect + HTTP_ACCEPT_LANGUAGE='en', + HTTP_USER_AGENT='Mozilla Gecko' + ) + thread = Thread.objects.all()[0] + self.assertEqual(1, thread.view_count) + + self.assertTrue(thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(thread._question_post()) + self.assertEqual(html, thread.get_cached_summary_html()) + + def test_question_upvote_downvote(self): + question = self.post_question() + question.score = 5 + question.vote_up_count = 7 + question.vote_down_count = 2 + question.save() + + self.client.logout() + self.client.login(username='user2', password='pswd') + response = self.client.post(urlresolvers.reverse('vote', kwargs={'id': question.id}), data={'type': '1'}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') # use AJAX request + self.assertEqual(200, response.status_code) + data = simplejson.loads(response.content) + + self.assertEqual(1, data['success']) + self.assertEqual(6, data['count']) # 6 == question.score(5) + 1 + + thread = Thread.objects.get(id=question.thread.id) + + self.assertTrue(thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(thread._question_post()) + self.assertEqual(html, thread.get_cached_summary_html()) + + ### + + response = self.client.post(urlresolvers.reverse('vote', kwargs={'id': question.id}), data={'type': '2'}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') # use AJAX request + self.assertEqual(200, response.status_code) + data = simplejson.loads(response.content) + + self.assertEqual(1, data['success']) + self.assertEqual(5, data['count']) # 6 == question.score(6) - 1 + + thread = Thread.objects.get(id=question.thread.id) + + self.assertTrue(thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(thread._question_post()) + self.assertEqual(html, thread.get_cached_summary_html()) + + def test_question_accept_answer(self): + question = self.post_question(user=self.user2) + answer = self.post_answer(question=question) + + self.client.logout() + self.client.login(username='user2', password='pswd') + response = self.client.post(urlresolvers.reverse('vote', kwargs={'id': question.id}), data={'type': '0', 'postId': answer.id}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') # use AJAX request + self.assertEqual(200, response.status_code) + data = simplejson.loads(response.content) + + self.assertEqual(1, data['success']) + + thread = Thread.objects.get(id=question.thread.id) + + self.assertTrue(thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(thread._question_post()) + self.assertEqual(html, thread.get_cached_summary_html()) + + +# TODO: (in spare time, these cases should already pass but we shold have them eventually for completness) +# - Publishing anonymous questions / answers +# - Re-posting question as answer and vice versa diff --git a/askbot/views/commands.py b/askbot/views/commands.py index f596b6f6..7db27ef2 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -205,6 +205,11 @@ def vote(request, id): response_data['status'] = 1 #cancelation else: request.user.accept_best_answer(answer) + + #################################################################### + answer.thread.update_summary_html() # regenerate question/thread summary html + #################################################################### + else: raise exceptions.PermissionDenied( _('Sorry, but anonymous users cannot accept answers') @@ -235,6 +240,11 @@ def vote(request, id): post = post ) + #################################################################### + if vote_type in ('1', '2'): # up/down-vote question + post.thread.update_summary_html() # regenerate question/thread summary html + #################################################################### + elif vote_type in ['7', '8']: #flag question or answer if vote_type == '7': diff --git a/askbot/views/readers.py b/askbot/views/readers.py index 88d2bf7d..c243f99c 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -10,13 +10,10 @@ import datetime import logging import urllib import operator -from django.contrib.auth.models import User from django.shortcuts import get_object_or_404 from django.http import HttpResponseRedirect, HttpResponse, Http404, HttpResponseNotAllowed from django.core.paginator import Paginator, EmptyPage, InvalidPage from django.template import Context -from django.template.base import Template -from django.template.context import RequestContext from django.utils import simplejson from django.utils.translation import ugettext as _ from django.utils.translation import ungettext @@ -42,7 +39,7 @@ from askbot.search.state_manager import SearchState from askbot.templatetags import extra_tags import askbot.conf from askbot.conf import settings as askbot_settings -from askbot.skins.loaders import render_into_skin, get_template#jinja2 template loading enviroment +from askbot.skins.loaders import render_into_skin, get_template #jinja2 template loading enviroment # used in index page #todo: - take these out of const or settings -- cgit v1.2.3-1-g7c22 From 02d52a2010847db712be5d2bb684a48fe211e7c1 Mon Sep 17 00:00:00 2001 From: Tomasz Zielinski Date: Tue, 24 Jan 2012 18:28:13 +0100 Subject: Main page thread summary caching BETA - updated TODO list --- askbot/tests/post_model_tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py index 44bcb10a..1cac808e 100644 --- a/askbot/tests/post_model_tests.py +++ b/askbot/tests/post_model_tests.py @@ -665,6 +665,7 @@ class ThreadRenderCacheUpdateTests(AskbotTestCase): self.assertEqual(html, thread.get_cached_summary_html()) -# TODO: (in spare time, these cases should already pass but we shold have them eventually for completness) +# TODO: (in spare time - those cases should pass without changing anything in code but we should have them eventually for completness) # - Publishing anonymous questions / answers # - Re-posting question as answer and vice versa +# - Management commands (like post_emailed_questions) \ No newline at end of file -- cgit v1.2.3-1-g7c22 From 6c6f43a4e5339aded1b8b1ae921411cb00c71e84 Mon Sep 17 00:00:00 2001 From: Tomasz Zielinski Date: Tue, 24 Jan 2012 19:05:05 +0100 Subject: Search bugfix attempt - still need assistance from Evgeny --- askbot/models/question.py | 17 +++++++++-------- askbot/views/readers.py | 11 ++++++++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/askbot/models/question.py b/askbot/models/question.py index cd1ca184..184e8813 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -98,24 +98,25 @@ class ThreadManager(models.Manager): return thread - def get_for_query(self, search_query): + def get_for_query(self, search_query, qs=None): """returns a query set of questions, matching the full text query """ + if not qs: + qs = self.all() # if getattr(settings, 'USE_SPHINX_SEARCH', False): # matching_questions = Question.sphinx_search.query(search_query) # question_ids = [q.id for q in matching_questions] -# return self.filter(posts__post_type='question', posts__deleted=False, posts__self_question_id__in=question_ids) +# return qs.filter(posts__post_type='question', posts__deleted=False, posts__self_question_id__in=question_ids) if askbot.get_database_engine_name().endswith('mysql') \ and mysql.supports_full_text_search(): - return self.filter( + return qs.filter( models.Q(title__search = search_query) | models.Q(tagnames__search = search_query) | models.Q(posts__deleted=False, posts__text__search = search_query) ) elif 'postgresql_psycopg2' in askbot.get_database_engine_name(): - # TODO: !! Fix Postgres search - rank_clause = "ts_rank(text_search_vector, plainto_tsquery(%s))"; + rank_clause = "ts_rank(text_search_vector, plainto_tsquery(%s))" search_query = '&'.join(search_query.split()) extra_params = (search_query,) extra_kwargs = { @@ -124,9 +125,9 @@ class ThreadManager(models.Manager): 'params': extra_params, 'select_params': extra_params, } - return self.extra(**extra_kwargs) + return qs.extra(**extra_kwargs) else: - return self.filter( + return qs.filter( models.Q(title__icontains=search_query) | models.Q(tagnames__icontains=search_query) | models.Q(posts__deleted=False, posts__text__icontains = search_query) @@ -148,7 +149,7 @@ class ThreadManager(models.Manager): meta_data = {} if search_state.stripped_query: - qs = self.get_for_query(search_state.stripped_query) + qs = self.get_for_query(search_query=search_state.stripped_query, qs=qs) if search_state.query_title: qs = qs.filter(title__icontains = search_state.query_title) if search_state.query_users: diff --git a/askbot/views/readers.py b/askbot/views/readers.py index c243f99c..e034533c 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -69,6 +69,9 @@ def questions(request, **kwargs): List of Questions, Tagged questions, and Unanswered questions. matching search query or user selection """ + import time + start = time.time() + if request.method != 'GET': return HttpResponseNotAllowed(['GET']) @@ -208,7 +211,12 @@ def questions(request, **kwargs): 'feed_url': context_feed_url, } - return render_into_skin('main_page.html', template_data, request) + tstart = time.time() + ret = render_into_skin('main_page.html', template_data, request) + print "Jinja - elapsed:", time.time() - tstart + print "Elapsed:", time.time() - start + return ret + def tags(request):#view showing a listing of available tags - plain list @@ -455,6 +463,7 @@ def question(request, id):#refactor - long subroutine. display question body, an page_objects = objects_list.page(show_page) #count visits + #import ipdb; ipdb.set_trace() if functions.not_a_robot_request(request): #todo: split this out into a subroutine #todo: merge view counts per user and per session -- cgit v1.2.3-1-g7c22 From eb765ca1625f1c82bf8b8e51c26a656c43255204 Mon Sep 17 00:00:00 2001 From: Tomasz Zielinski Date: Tue, 24 Jan 2012 19:16:17 +0100 Subject: Search bugfix done + reverted accidential commit of timing code --- askbot/models/question.py | 4 ++-- askbot/views/readers.py | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/askbot/models/question.py b/askbot/models/question.py index 184e8813..6c2fa383 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -116,12 +116,12 @@ class ThreadManager(models.Manager): models.Q(posts__deleted=False, posts__text__search = search_query) ) elif 'postgresql_psycopg2' in askbot.get_database_engine_name(): - rank_clause = "ts_rank(text_search_vector, plainto_tsquery(%s))" + rank_clause = "ts_rank(askbot_thread.text_search_vector, plainto_tsquery(%s))" search_query = '&'.join(search_query.split()) extra_params = (search_query,) extra_kwargs = { 'select': {'relevance': rank_clause}, - 'where': ['text_search_vector @@ plainto_tsquery(%s)'], + 'where': ['askbot_thread.text_search_vector @@ plainto_tsquery(%s)'], 'params': extra_params, 'select_params': extra_params, } diff --git a/askbot/views/readers.py b/askbot/views/readers.py index e034533c..cccfce67 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -69,9 +69,6 @@ def questions(request, **kwargs): List of Questions, Tagged questions, and Unanswered questions. matching search query or user selection """ - import time - start = time.time() - if request.method != 'GET': return HttpResponseNotAllowed(['GET']) @@ -211,11 +208,7 @@ def questions(request, **kwargs): 'feed_url': context_feed_url, } - tstart = time.time() - ret = render_into_skin('main_page.html', template_data, request) - print "Jinja - elapsed:", time.time() - tstart - print "Elapsed:", time.time() - start - return ret + return render_into_skin('main_page.html', template_data, request) def tags(request):#view showing a listing of available tags - plain list -- cgit v1.2.3-1-g7c22 From efbe12a0598ba767f5065ae94ec0a5c1be256c45 Mon Sep 17 00:00:00 2001 From: Jordi Bofill Date: Wed, 25 Jan 2012 12:07:29 +0100 Subject: fit text --- askbot/locale/ca/LC_MESSAGES/django.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/askbot/locale/ca/LC_MESSAGES/django.po b/askbot/locale/ca/LC_MESSAGES/django.po index f65be5f3..e365b17f 100644 --- a/askbot/locale/ca/LC_MESSAGES/django.po +++ b/askbot/locale/ca/LC_MESSAGES/django.po @@ -6216,14 +6216,14 @@ msgstr "dóna detalls suficients" #: skins/default/templates/widgets/question_summary.html:12 msgid "view" msgid_plural "views" -msgstr[0] "vista" +msgstr[0] "visita" msgstr[1] "visites" #: skins/default/templates/widgets/question_summary.html:29 msgid "answer" msgid_plural "answers" -msgstr[0] "resposta" -msgstr[1] "respostes" +msgstr[0] "resp." +msgstr[1] "resp." #: skins/default/templates/widgets/question_summary.html:40 msgid "vote" -- cgit v1.2.3-1-g7c22 From 0d034b5e39bba4f36474591632ffde61bcfe189c Mon Sep 17 00:00:00 2001 From: Jordi Bofill Date: Wed, 25 Jan 2012 14:21:51 +0100 Subject: more translations ... --- askbot/locale/ca/LC_MESSAGES/django.po | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/askbot/locale/ca/LC_MESSAGES/django.po b/askbot/locale/ca/LC_MESSAGES/django.po index e365b17f..e7fd6aca 100644 --- a/askbot/locale/ca/LC_MESSAGES/django.po +++ b/askbot/locale/ca/LC_MESSAGES/django.po @@ -2141,7 +2141,7 @@ msgstr "completat perfil d'usuari" #: const/__init__.py:139 msgid "email update sent to user" -msgstr "enviat missatge d'actualitació a l'usuari" +msgstr "enviat missatge d'actualització a l'usuari" #: const/__init__.py:142 msgid "reminder about unanswered questions sent" @@ -2375,23 +2375,18 @@ msgid "" "password." msgstr "" -# msgstr "" -# "La contrasenya antiga és incorrecta. Introduïu la contrasenya correcta." #: deps/django_authopenid/forms.py:399 msgid "Sorry, we don't have this email address in the database" -msgstr "" +msgstr "Aquesta adreça de correu no figura a la base de dades" -# msgstr "Adreça de correu electrònic inexistent a la base de dades" #: deps/django_authopenid/forms.py:435 msgid "Your user name (required)" -msgstr "" +msgstr "Nom d'usuari (requerit)" -# msgstr "El vostre nom d'usuari (requerit)" #: deps/django_authopenid/forms.py:450 msgid "Incorrect username." -msgstr "" +msgstr "Nom d'usuari incorrecta" -# msgstr "Nom d'usuari inexistent" #: deps/django_authopenid/urls.py:9 deps/django_authopenid/urls.py:12 #: deps/django_authopenid/urls.py:15 setup_templates/settings.py:208 msgid "signin/" @@ -6142,7 +6137,7 @@ msgstr "fes una pregunta" #: skins/default/templates/widgets/ask_form.html:6 msgid "login to post question info" -msgstr "entrar per publicar una pregunta2" +msgstr "cal entrar per publicar una pregunta" #: skins/default/templates/widgets/ask_form.html:10 #, python-format @@ -6412,24 +6407,24 @@ msgstr "Per avui li queden %(votes_left)s restants" #: views/commands.py:123 msgid "Sorry, but anonymous users cannot access the inbox" -msgstr "" +msgstr "Els usuaris anònims no tenen accés a la safata d'entrada" #: views/commands.py:198 msgid "Sorry, something is not right here..." -msgstr "" +msgstr "alguna cosa no funciona aqui ..." #: views/commands.py:213 msgid "Sorry, but anonymous users cannot accept answers" -msgstr "" +msgstr "Els usuaris anònims no poden acceptar respostes" #: views/commands.py:320 #, python-format msgid "subscription saved, %(email)s needs validation, see %(details_url)s" -msgstr "" +msgstr "subscripció desada, %(email)s s'han de validar, veure %(details_url)s" #: views/commands.py:327 msgid "email update frequency has been set to daily" -msgstr "" +msgstr "freqüencia d'actualització de correus diària" #: views/commands.py:433 #, python-format @@ -6475,7 +6470,7 @@ msgstr[1] "%(badge_count)d %(badge_level)s insígnies" msgid "" "Sorry, the comment you are looking for has been deleted and is no longer " "accessible" -msgstr "" +msgstr "el commentari que busca s'ha esborrat i no es pot accedir" #: views/users.py:212 msgid "moderate user" -- cgit v1.2.3-1-g7c22 From 820e0ac58a94580ba31fcd0a52ee2258c1141970 Mon Sep 17 00:00:00 2001 From: Jordi Bofill Date: Wed, 25 Jan 2012 14:31:25 +0100 Subject: new catalan .mo file --- askbot/locale/ca/LC_MESSAGES/django.mo | Bin 90367 -> 91439 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/askbot/locale/ca/LC_MESSAGES/django.mo b/askbot/locale/ca/LC_MESSAGES/django.mo index 1be5bb52..71dbc41c 100644 Binary files a/askbot/locale/ca/LC_MESSAGES/django.mo and b/askbot/locale/ca/LC_MESSAGES/django.mo differ -- cgit v1.2.3-1-g7c22 From 203fa05371c32bac09020fd4903addbaa8de8526 Mon Sep 17 00:00:00 2001 From: Tomasz Zielinski Date: Thu, 26 Jan 2012 01:25:31 +0100 Subject: Upgraded migrations to make them work for MySQL+InnoDB - BETA version --- .../0002_make_multiple_openids_possible.py | 7 +++ .../migrations/0012_delete_some_unused_models.py | 8 +-- askbot/migrations/0101_megadeath_of_q_a_c.py | 14 ++++- .../migrations/0102_rename_post_fields_back_1.py | 15 +++++- .../migrations/0103_rename_post_fields_back_2.py | 15 +++++- ...ute_question_post__add_field_repute_question.py | 15 +++++- askbot/migrations/0105_restore_anon_ans_q.py | 15 +++++- .../0106_update_postgres_full_text_setup.py | 3 +- askbot/migrations/__init__.py | 62 ++++++++++++++++++++++ 9 files changed, 142 insertions(+), 12 deletions(-) diff --git a/askbot/deps/django_authopenid/migrations/0002_make_multiple_openids_possible.py b/askbot/deps/django_authopenid/migrations/0002_make_multiple_openids_possible.py index 4e615e65..e5541286 100644 --- a/askbot/deps/django_authopenid/migrations/0002_make_multiple_openids_possible.py +++ b/askbot/deps/django_authopenid/migrations/0002_make_multiple_openids_possible.py @@ -4,6 +4,9 @@ from south.db import db from south.v2 import SchemaMigration from django.db import models +from askbot.migrations import houston_do_we_have_a_problem + + class Migration(SchemaMigration): def forwards(self, orm): @@ -12,6 +15,10 @@ class Migration(SchemaMigration): db.add_column('django_authopenid_userassociation', 'provider_name', self.gf('django.db.models.fields.CharField')(default='unknown', max_length=64), keep_default=False) # Removing unique constraint on 'UserAssociation', fields ['user'] + if houston_do_we_have_a_problem('django_authopenid_userassociation'): + # In MySQL+InnoDB Foreign keys have to have some index on them, + # therefore before deleting the UNIQUE index we have to create an "ordinary" one + db.create_index('django_authopenid_userassociation', ['user_id']) db.delete_unique('django_authopenid_userassociation', ['user_id']) # Adding unique constraint on 'UserAssociation', fields ['provider_name', 'user'] diff --git a/askbot/migrations/0012_delete_some_unused_models.py b/askbot/migrations/0012_delete_some_unused_models.py index 8c0edf8e..3a0888ef 100644 --- a/askbot/migrations/0012_delete_some_unused_models.py +++ b/askbot/migrations/0012_delete_some_unused_models.py @@ -24,15 +24,15 @@ class Migration(SchemaMigration): # Deleting model 'Mention' db.delete_table(u'mention') - # Deleting model 'Book' - db.delete_table(u'book') - # Removing M2M table for field questions on 'Book' db.delete_table('book_question') # Deleting model 'BookAuthorRss' db.delete_table(u'book_author_rss') - + + # Deleting model 'Book' + db.delete_table(u'book') + def backwards(self, orm): diff --git a/askbot/migrations/0101_megadeath_of_q_a_c.py b/askbot/migrations/0101_megadeath_of_q_a_c.py index 7e63a999..2f04b4e4 100644 --- a/askbot/migrations/0101_megadeath_of_q_a_c.py +++ b/askbot/migrations/0101_megadeath_of_q_a_c.py @@ -5,12 +5,22 @@ from south.db import db from south.v2 import SchemaMigration from django.db import models -from askbot.migrations import TERM_YELLOW, TERM_RESET +from askbot.migrations import TERM_YELLOW, TERM_RESET, innodb_ready_rename_column + class Migration(SchemaMigration): def forwards(self, orm): - db.rename_column('askbot_thread', 'accepted_answer_post_id', 'accepted_answer_id') + #db.rename_column('askbot_thread', 'accepted_answer_post_id', 'accepted_answer_id') + innodb_ready_rename_column( + orm=orm, + models=self.__class__.models, + table='askbot_thread', + old_column_name='accepted_answer_post_id', + new_column_name='accepted_answer_id', + app_model='askbot.thread', + new_field_name='accepted_answer' + ) # Deleting model 'Comment' db.delete_table(u'comment') diff --git a/askbot/migrations/0102_rename_post_fields_back_1.py b/askbot/migrations/0102_rename_post_fields_back_1.py index 9d155ddd..9c51aac6 100644 --- a/askbot/migrations/0102_rename_post_fields_back_1.py +++ b/askbot/migrations/0102_rename_post_fields_back_1.py @@ -4,10 +4,23 @@ from south.db import db from south.v2 import SchemaMigration from django.db import models +from askbot.migrations import innodb_ready_rename_column + + class Migration(SchemaMigration): def forwards(self, orm): - db.rename_column('askbot_questionview', 'question_post_id', 'question_id') + #db.rename_column('askbot_questionview', 'question_post_id', 'question_id') + innodb_ready_rename_column( + orm=orm, + models=self.__class__.models, + table='askbot_questionview', + old_column_name='question_post_id', + new_column_name='question_id', + app_model='askbot.questionview', + new_field_name='question' + ) + def backwards(self, orm): diff --git a/askbot/migrations/0103_rename_post_fields_back_2.py b/askbot/migrations/0103_rename_post_fields_back_2.py index 6640ff83..f56e2258 100644 --- a/askbot/migrations/0103_rename_post_fields_back_2.py +++ b/askbot/migrations/0103_rename_post_fields_back_2.py @@ -4,10 +4,23 @@ from south.db import db from south.v2 import SchemaMigration from django.db import models +from askbot.migrations import innodb_ready_rename_column + + class Migration(SchemaMigration): def forwards(self, orm): - db.rename_column(u'activity', 'question_post_id', 'question_id') + #db.rename_column(u'activity', 'question_post_id', 'question_id') + innodb_ready_rename_column( + orm=orm, + models=self.__class__.models, + table='activity', + old_column_name='question_post_id', + new_column_name='question_id', + app_model='askbot.activity', + new_field_name='question' + ) + def backwards(self, orm): diff --git a/askbot/migrations/0104_auto__del_field_repute_question_post__add_field_repute_question.py b/askbot/migrations/0104_auto__del_field_repute_question_post__add_field_repute_question.py index 4044cef1..ed72e1ec 100644 --- a/askbot/migrations/0104_auto__del_field_repute_question_post__add_field_repute_question.py +++ b/askbot/migrations/0104_auto__del_field_repute_question_post__add_field_repute_question.py @@ -4,10 +4,23 @@ from south.db import db from south.v2 import SchemaMigration from django.db import models +from askbot.migrations import innodb_ready_rename_column + + class Migration(SchemaMigration): def forwards(self, orm): - db.rename_column(u'repute', 'question_post_id', 'question_id') + #db.rename_column(u'repute', 'question_post_id', 'question_id') + innodb_ready_rename_column( + orm=orm, + models=self.__class__.models, + table='repute', + old_column_name='question_post_id', + new_column_name='question_id', + app_model='askbot.repute', + new_field_name='question' + ) + def backwards(self, orm): diff --git a/askbot/migrations/0105_restore_anon_ans_q.py b/askbot/migrations/0105_restore_anon_ans_q.py index 05429728..4bf5ca99 100644 --- a/askbot/migrations/0105_restore_anon_ans_q.py +++ b/askbot/migrations/0105_restore_anon_ans_q.py @@ -4,10 +4,23 @@ from south.db import db from south.v2 import SchemaMigration from django.db import models +from askbot.migrations import innodb_ready_rename_column + + class Migration(SchemaMigration): def forwards(self, orm): - db.rename_column('askbot_anonymousanswer', 'question_post_id', 'question_id') + #db.rename_column('askbot_anonymousanswer', 'question_post_id', 'question_id') + innodb_ready_rename_column( + orm=orm, + models=self.__class__.models, + table='askbot_anonymousanswer', + old_column_name='question_post_id', + new_column_name='question_id', + app_model='askbot.anonymousanswer', + new_field_name='question' + ) + def backwards(self, orm): diff --git a/askbot/migrations/0106_update_postgres_full_text_setup.py b/askbot/migrations/0106_update_postgres_full_text_setup.py index b0b0c668..e788879c 100644 --- a/askbot/migrations/0106_update_postgres_full_text_setup.py +++ b/askbot/migrations/0106_update_postgres_full_text_setup.py @@ -15,8 +15,7 @@ class Migration(DataMigration): def forwards(self, orm): "Write your forwards methods here." - if 'test' in sys.argv: - return # Somehow this migration fails when called from test runner + return # TODO: remove me when the SQL is fixed! if 'postgresql_psycopg2' in askbot.get_database_engine_name(): script_path = os.path.join( diff --git a/askbot/migrations/__init__.py b/askbot/migrations/__init__.py index ac6b3d47..86377b7b 100644 --- a/askbot/migrations/__init__.py +++ b/askbot/migrations/__init__.py @@ -1,3 +1,6 @@ +from south.db import db +from south.utils import ask_for_it_by_name + # Terminal ANSI codes for printing colored text: # - http://code.google.com/p/testoob/source/browse/trunk/src/testoob/reporting/colored.py#20 # - http://stackoverflow.com/questions/287871/print-in-terminal-with-colors-using-python @@ -5,3 +8,62 @@ TERM_RED_BOLD = '\x1b[31;01m\x1b[01m' TERM_YELLOW = "\x1b[33;01m" TERM_GREEN = "\x1b[32;06m" TERM_RESET = '\x1b[0m' + +def houston_do_we_have_a_problem(table): + "Checks if we're using MySQL + InnoDB" + if not db.dry_run and db.backend_name == 'mysql': + db_table = [db._get_connection().settings_dict['NAME'], table] + ret = db.execute( + "SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES " + "where TABLE_SCHEMA = %s and TABLE_NAME = %s", + db_table + ) + assert len(ret) == 1 # There HAVE to be info about this table ! + assert len(ret[0]) == 2 + if ret[0][1] == 'InnoDB': + print TERM_YELLOW, "!!!", '.'.join(db_table), "is InnoDB - using workarounds !!!", TERM_RESET + return True + return False + + +def innodb_ready_rename_column(orm, models, table, old_column_name, new_column_name, app_model, new_field_name): + """ + Foreign key renaming which works for InnoDB + More: http://south.aeracode.org/ticket/466 + + Parameters: + - orm: a reference to 'orm' parameter passed to Migration.forwards()/backwards() + - models: reference to Migration.models data structure + - table: e.g. 'askbot_thread' + - old_column_name: e.g. 'question_post_id' + - new_column_name: e.g. 'question_id' + - app_model: e.g. 'askbot.thread' (should be a dict key into 'models') + - new_field_name: e.g. 'question' (usually it's same as new_column_name, only without trailing '_id') + """ + use_workaround = houston_do_we_have_a_problem(table) + + # ditch the foreign key + if use_workaround: + db.delete_foreign_key(table, old_column_name) + + # rename column + db.rename_column(table, old_column_name, new_column_name) + + # restore the foreign key + if not use_workaround: + return + + model_def = models[app_model][new_field_name] + assert model_def[0] == 'django.db.models.fields.related.ForeignKey' + # Copy the dict so that we don't change the original + # (otherwise the dry run would change it for the "normal" run + # and the latter would try to convert str to model once again) + fkey_params = model_def[2].copy() + assert 'to' in fkey_params + assert fkey_params['to'].startswith("orm['") + assert fkey_params['to'].endswith("']") + fkey_params['to'] = orm[fkey_params['to'][5:-2]] # convert "orm['app.models']" string to actual model + field = ask_for_it_by_name(model_def[0])(**fkey_params) + # INFO: ask_for_it_by_name() if equivalent to self.gf() which is usually used in migrations, e.g.: + # db.alter_column('askbot_badgedata', 'slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=50)) + db.alter_column(table, new_column_name, field) -- cgit v1.2.3-1-g7c22 From 4beb956905d6094a7ffd0d5a50797904f9aec943 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Wed, 25 Jan 2012 23:36:39 -0300 Subject: added "left" sidebar option --- askbot/conf/__init__.py | 1 + askbot/conf/leading_sidebar.py | 38 +++++++++++++++++++++++++++++ askbot/skins/default/media/style/style.less | 4 +++ askbot/skins/default/templates/base.html | 5 ++++ 4 files changed, 48 insertions(+) create mode 100644 askbot/conf/leading_sidebar.py diff --git a/askbot/conf/__init__.py b/askbot/conf/__init__.py index 026a6185..de1eeccc 100644 --- a/askbot/conf/__init__.py +++ b/askbot/conf/__init__.py @@ -13,6 +13,7 @@ import askbot.conf.skin_general_settings import askbot.conf.sidebar_main import askbot.conf.sidebar_question import askbot.conf.sidebar_profile +import askbot.conf.leading_sidebar import askbot.conf.spam_and_moderation import askbot.conf.user_settings import askbot.conf.markup diff --git a/askbot/conf/leading_sidebar.py b/askbot/conf/leading_sidebar.py new file mode 100644 index 00000000..b3909961 --- /dev/null +++ b/askbot/conf/leading_sidebar.py @@ -0,0 +1,38 @@ +""" +Sidebar settings +""" +from askbot.conf.settings_wrapper import settings +from askbot.deps.livesettings import ConfigurationGroup +from askbot.deps.livesettings import values +from django.utils.translation import ugettext as _ +from askbot.conf.super_groups import CONTENT_AND_UI + +LEADING_SIDEBAR = ConfigurationGroup( + 'LEADING_SIDEBAR', + _('Common left sidebar'), + super_group = CONTENT_AND_UI + ) + +settings.register( + values.BooleanValue( + LEADING_SIDEBAR, + 'ENABLE_LEADING_SIDEBAR', + description = _('Enable left sidebar'), + default = False, + ) +) + +settings.register( + values.LongStringValue( + LEADING_SIDEBAR, + 'LEADING_SIDEBAR', + description = _('HTML for the left sidebar'), + default = '', + help_text = _( + 'Use this area to enter content at the LEFT sidebar' + 'in HTML format. When using this option, please ' + 'use the HTML validation service to make sure that ' + 'your input is valid and works well in all browsers.' + ) + ) +) diff --git a/askbot/skins/default/media/style/style.less b/askbot/skins/default/media/style/style.less index 389a0acc..7b564d8a 100644 --- a/askbot/skins/default/media/style/style.less +++ b/askbot/skins/default/media/style/style.less @@ -3347,3 +3347,7 @@ pre.prettyprint { clear:both;padding: 3px; border: 0px solid #888; } .atn { color: #404; } .atv { color: #060; } } + +#leading-sidebar { + float: left; +} diff --git a/askbot/skins/default/templates/base.html b/askbot/skins/default/templates/base.html index 39b89fe8..348ab23a 100644 --- a/askbot/skins/default/templates/base.html +++ b/askbot/skins/default/templates/base.html @@ -25,6 +25,11 @@ {% endif %} {% include "widgets/header.html" %} {# Logo, user tool navigation and meta navitation #} {% include "widgets/secondary_header.html" %} {# Scope selector, search input and ask button #} + {% if settings.ENABLE_LEADING_SIDEBAR %} +
+ {{ settings.LEADING_SIDEBAR|safe }} +
+ {% endif %}
{% block body %} {% endblock %} -- cgit v1.2.3-1-g7c22 From 11a5efc0185acf0d50a946053d81fb29a458a0f7 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Wed, 25 Jan 2012 23:37:42 -0300 Subject: added left sidebar option --- askbot/doc/source/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index d9c85e1b..c6d29ea8 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -8,6 +8,7 @@ Development version (not released yet) * Added tests for the CSRF_COOKIE_DOMAIN setting in the startup_procedures (Evgeny) * Askbot now respects django's staticfiles app (Radim Řehůřek, Evgeny) * Fixed the url translation bug (Evgeny) +* Added left sidebar option (Evgeny) 0.7.39 (Jan 11, 2012) --------------------- -- cgit v1.2.3-1-g7c22 From 92451a78e2fb5009e088beea6b741966619075ff Mon Sep 17 00:00:00 2001 From: Tomasz Zielinski Date: Thu, 26 Jan 2012 13:49:07 +0100 Subject: Adjusted code to make unit tests pass --- .gitignore | 1 + .../templates/widgets/question_summary.html | 3 +- askbot/tests/page_load_tests.py | 34 +++++++++++++++------- askbot/tests/post_model_tests.py | 7 ++++- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index dc9e2f72..586c4c8a 100755 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ tmp/* /manage.py /urls.py /log +/prof load askbot/skins/default/media/js/flot askbot/skins/common/media/js/closure/google-closure diff --git a/askbot/skins/default/templates/widgets/question_summary.html b/askbot/skins/default/templates/widgets/question_summary.html index 6ab59de5..56154847 100644 --- a/askbot/skins/default/templates/widgets/question_summary.html +++ b/askbot/skins/default/templates/widgets/question_summary.html @@ -42,7 +42,8 @@
- {{ thread.last_activity_at|diff_date }} + {# We have to kill microseconds below because InnoDB doesn't support them and all kinds of funny things happen in unit tests #} + {{ thread.last_activity_at|diff_date }} {% if question.is_anonymous %} {{ thread.last_activity_by.get_anonymous_name() }} {% else %} diff --git a/askbot/tests/page_load_tests.py b/askbot/tests/page_load_tests.py index 18d8d69c..10bded11 100644 --- a/askbot/tests/page_load_tests.py +++ b/askbot/tests/page_load_tests.py @@ -3,6 +3,8 @@ from django.test import signals from django.conf import settings from django.core.urlresolvers import reverse from django.core import management +from django.core.cache.backends.dummy import DummyCache +from django.core import cache import coffin import coffin.template @@ -63,6 +65,13 @@ class PageLoadTestCase(AskbotTestCase): ############################################# + def setUp(self): + self.old_cache = cache.cache + cache.cache = DummyCache('', {}) # Disable caching (to not interfere with production cache, not sure if that's possible but let's not risk it) + + def tearDown(self): + cache.cache = self.old_cache # Restore caching + def try_url( self, url_name, status_code=200, template=None, @@ -99,16 +108,18 @@ class PageLoadTestCase(AskbotTestCase): #asuming that there is more than one template template_names = ','.join([t.name for t in r.template]) print 'templates are %s' % template_names - if follow == False: - self.fail( - ('Have issue accessing %s. ' - 'This should not have happened, ' - 'since you are not expecting a redirect ' - 'i.e. follow == False, there should be only ' - 'one template') % url - ) - - #self.assertEqual(r.template[0].name, template) + # The following code is no longer relevant because we're using + # additional templates for cached fragments [e.g. thread.get_summary_html()] +# if follow == False: +# self.fail( +# ('Have issue accessing %s. ' +# 'This should not have happened, ' +# 'since you are not expecting a redirect ' +# 'i.e. follow == False, there should be only ' +# 'one template') % url +# ) +# +# self.assertEqual(r.template[0].name, template) self.assertIn(template, [t.name for t in r.template]) else: raise Exception('unexpected error while runnig test') @@ -120,7 +131,8 @@ class PageLoadTestCase(AskbotTestCase): self.assertEqual(response.status_code, 200) self.failUnless(len(response.redirect_chain) == 1) self.failUnless(response.redirect_chain[0][0].endswith('/questions/')) - self.assertEquals(response.template.name, 'main_page.html') + self.assertTrue(isinstance(response.template, list)) + self.assertIn('main_page.html', [t.name for t in response.template]) def proto_test_ask_page(self, allow_anonymous, status_code): prev_setting = askbot_settings.ALLOW_POSTING_BEFORE_LOGGING_IN diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py index 1cac808e..06bceca1 100644 --- a/askbot/tests/post_model_tests.py +++ b/askbot/tests/post_model_tests.py @@ -528,7 +528,10 @@ class ThreadRenderCacheUpdateTests(AskbotTestCase): question = self.post_question() self.assertEqual(1, Post.objects.count()) - thread = question.thread + #thread = question.thread + # get fresh Thread instance so that on MySQL it has timestamps without microseconds + thread = Thread.objects.get(id=question.thread.id) + self.assertEqual(0, thread.answer_count) self.assertEqual(thread.last_activity_at, question.added_at) self.assertEqual(thread.last_activity_by, question.author) @@ -558,6 +561,8 @@ class ThreadRenderCacheUpdateTests(AskbotTestCase): def test_edit_answer(self): self.assertEqual(0, Post.objects.count()) question = self.post_question() + # get fresh question Post instance so that on MySQL it has timestamps without microseconds + question = Post.objects.get(id=question.id) self.assertEqual(question.thread.last_activity_at, question.added_at) self.assertEqual(question.thread.last_activity_by, question.author) -- cgit v1.2.3-1-g7c22 From 5768de932d9e8fdc7f9b999b91a82a7fbf0396c8 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 26 Jan 2012 11:37:16 -0300 Subject: changed default to always show the password form on login --- askbot/conf/login_providers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/conf/login_providers.py b/askbot/conf/login_providers.py index 3fab7d6a..23f1a86d 100644 --- a/askbot/conf/login_providers.py +++ b/askbot/conf/login_providers.py @@ -27,7 +27,7 @@ settings.register( livesettings.BooleanValue( LOGIN_PROVIDERS, 'SIGNIN_ALWAYS_SHOW_LOCAL_LOGIN', - default = False, + default = True, description=_('Always display local login form and hide "Askbot" button.'), ) ) -- cgit v1.2.3-1-g7c22 From c9b50fd4536e6da56d28a0d53a4ce1b634c739e9 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 26 Jan 2012 15:46:04 -0300 Subject: updated default style.css --- askbot/skins/default/media/style/style.css | 3670 ++++++++++++++++++++++++---- 1 file changed, 3154 insertions(+), 516 deletions(-) diff --git a/askbot/skins/default/media/style/style.css b/askbot/skins/default/media/style/style.css index 4f886a4c..d52c4f11 100644 --- a/askbot/skins/default/media/style/style.css +++ b/askbot/skins/default/media/style/style.css @@ -1,517 +1,3155 @@ @import url(jquery.autocomplete.css); -body{background:#FFF;font-size:14px;line-height:150%;margin:0;padding:0;color:#000;font-family:Arial;} -div{margin:0 auto;padding:0;} -h1,h2,h3,h4,h5,h6,ul,li,dl,dt,dd,form,img,p{margin:0;padding:0;border:none;} -label{vertical-align:middle;} -hr{border:none;border-top:1px dashed #ccccce;} -input,select{vertical-align:middle;font-family:Trebuchet MS,"segoe ui",Helvetica,Tahoma,Verdana,MingLiu,PMingLiu,Arial,sans-serif;margin-left:0px;} -textarea:focus,input:focus{outline:none;} -iframe{border:none;} -p{font-size:14px;line-height:140%;margin-bottom:6px;} -a{color:#1b79bd;text-decoration:none;cursor:pointer;} -h2{font-size:21px;padding:3px 0 3px 5px;} -h3{font-size:19px;padding:3px 0 3px 5px;} -ul{list-style:disc;margin-left:20px;padding-left:0px;margin-bottom:1em;} -ol{list-style:decimal;margin-left:30px;margin-bottom:1em;padding-left:0px;} -td ul{vertical-align:middle;} -li input{margin:3px 3px 4px 3px;} -pre{font-family:Consolas, Monaco, Liberation Mono, Lucida Console, Monospace;font-size:100%;margin-bottom:10px;background-color:#F5F5F5;padding-left:5px;padding-top:5px;padding-bottom:20px ! ie7;} -code{font-family:Consolas, Monaco, Liberation Mono, Lucida Console, Monospace;font-size:100%;} -blockquote{margin-bottom:10px;margin-right:15px;padding:10px 0px 1px 10px;background-color:#F5F5F5;} -* html .clearfix,* html .paginator{height:1;overflow:visible;} -+html .clearfix,+html .paginator{min-height:1%;} -.clearfix:after,.paginator:after{clear:both;content:".";display:block;height:0;visibility:hidden;} -.badges a{color:#763333;text-decoration:underline;} -a:hover{text-decoration:underline;} -.badge-context-toggle.active{cursor:pointer;text-decoration:underline;} -h1{font-size:24px;padding:10px 0 5px 0px;} -body.user-messages{margin-top:2.4em;} -.left{float:left;} -.right{float:right;} -.clean{clear:both;} -.center{margin:0 auto;padding:0;} -.notify{position:fixed;top:0px;left:0px;width:100%;z-index:100;padding:0;text-align:center;background-color:#f5dd69;border-top:#fff 1px solid;font-family:'Yanone Kaffeesatz',sans-serif;}.notify p.notification{margin-top:6px;margin-bottom:6px;font-size:16px;color:#424242;} -#closeNotify{position:absolute;right:5px;top:7px;color:#735005;text-decoration:none;line-height:18px;background:-6px -5px url(../images/sprites.png) no-repeat;cursor:pointer;width:20px;height:20px;} -#closeNotify:hover{background:-26px -5px url(../images/sprites.png) no-repeat;} -#header{margin-top:0px;background:#16160f;font-family:'Yanone Kaffeesatz',sans-serif;} -.content-wrapper{width:960px;margin:auto;position:relative;} -#logo img{padding:5px 0px 5px 0px;height:75px;width:auto;float:left;} -#userToolsNav{height:20px;padding-bottom:5px;}#userToolsNav a{height:35px;text-align:right;margin-left:20px;text-decoration:underline;color:#d0e296;font-size:16px;} -#userToolsNav a:first-child{margin-left:0;} -#userToolsNav a#ab-responses{margin-left:3px;} -#userToolsNav .user-info,#userToolsNav .user-micro-info{color:#b5b593;} -#userToolsNav a img{vertical-align:middle;margin-bottom:2px;} -#userToolsNav .user-info a{margin:0;text-decoration:none;} -#metaNav{float:right;}#metaNav a{color:#e2e2ae;padding:0px 0px 0px 35px;height:25px;line-height:30px;margin:5px 0px 0px 10px;font-size:18px;font-weight:100;text-decoration:none;display:block;float:left;} -#metaNav a:hover{text-decoration:underline;} -#metaNav a.on{font-weight:bold;color:#FFF;text-decoration:none;} -#metaNav a.special{font-size:18px;color:#B02B2C;font-weight:bold;text-decoration:none;} -#metaNav a.special:hover{text-decoration:underline;} -#metaNav #navTags{background:-50px -5px url(../images/sprites.png) no-repeat;} -#metaNav #navUsers{background:-125px -5px url(../images/sprites.png) no-repeat;} -#metaNav #navBadges{background:-210px -5px url(../images/sprites.png) no-repeat;} -#header.with-logo #userToolsNav{position:absolute;bottom:0;right:0px;} -#header.without-logo #userToolsNav{float:left;margin-top:7px;} -#header.without-logo #metaNav{margin-bottom:7px;} -#secondaryHeader{height:55px;background:#e9e9e1;border-bottom:#d3d3c2 1px solid;border-top:#fcfcfc 1px solid;margin-bottom:10px;font-family:'Yanone Kaffeesatz',sans-serif;}#secondaryHeader #homeButton{border-right:#afaf9e 1px solid;background:-6px -36px url(../images/sprites.png) no-repeat;height:55px;width:43px;display:block;float:left;} -#secondaryHeader #homeButton:hover{background:-51px -36px url(../images/sprites.png) no-repeat;} -#secondaryHeader #scopeWrapper{width:688px;float:left;}#secondaryHeader #scopeWrapper a{display:block;float:left;} -#secondaryHeader #scopeWrapper .scope-selector{font-size:21px;color:#5a5a4b;height:55px;line-height:55px;margin-left:24px;} -#secondaryHeader #scopeWrapper .on{background:url(../images/scopearrow.png) no-repeat center bottom;} -#secondaryHeader #scopeWrapper .ask-message{font-size:24px;} -#searchBar{display:inline-block;background-color:#fff;width:412px;border:1px solid #c9c9b5;float:right;height:42px;margin:6px 0px 0px 15px;}#searchBar .searchInput,#searchBar .searchInputCancelable{font-size:30px;height:40px;font-weight:300;background:#FFF;border:0px;color:#484848;padding-left:10px;font-family:Arial;vertical-align:middle;} -#searchBar .searchInput{width:352px;} -#searchBar .searchInputCancelable{width:317px;} -#searchBar .logoutsearch{width:337px;} -#searchBar .searchBtn{font-size:10px;color:#666;background-color:#eee;height:42px;border:#FFF 1px solid;line-height:22px;text-align:center;float:right;margin:0px;width:48px;background:-98px -36px url(../images/sprites.png) no-repeat;cursor:pointer;} -#searchBar .searchBtn:hover{background:-146px -36px url(../images/sprites.png) no-repeat;} -#searchBar .cancelSearchBtn{font-size:30px;color:#ce8888;background:#fff;height:42px;border:0px;border-left:#deded0 1px solid;text-align:center;width:35px;cursor:pointer;} -#searchBar .cancelSearchBtn:hover{color:#d84040;} -body.anon #searchBar{width:500px;}body.anon #searchBar .searchInput{width:440px;} -body.anon #searchBar .searchInputCancelable{width:405px;} -#askButton{background:url(../images/bigbutton.png) repeat-x bottom;line-height:44px;text-align:center;width:200px;height:42px;font-size:23px;color:#4a757f;margin-top:7px;float:right;text-transform:uppercase;border-radius:5px;-ms-border-radius:5px;-moz-border-radius:5px;-webkit-border-radius:5px;-khtml-border-radius:5px;-webkit-box-shadow:1px 1px 2px #636363;-moz-box-shadow:1px 1px 2px #636363;box-shadow:1px 1px 2px #636363;} -#askButton:hover{text-decoration:none;background:url(../images/bigbutton.png) repeat-x top;text-shadow:0px 1px 0px #c6d9dd;-moz-text-shadow:0px 1px 0px #c6d9dd;-webkit-text-shadow:0px 1px 0px #c6d9dd;} -#ContentLeft{width:730px;float:left;position:relative;padding-bottom:10px;} -#ContentRight{width:200px;float:right;padding:0 0px 10px 0px;} -#ContentFull{float:left;width:960px;} -.box{background:#fff;padding:4px 0px 10px 0px;width:200px;}.box p{margin-bottom:4px;} -.box p.info-box-follow-up-links{text-align:right;margin:0;} -.box h2{padding-left:0;background:#eceeeb;height:30px;line-height:30px;text-align:right;font-size:18px !important;font-weight:normal;color:#656565;padding-right:10px;margin-bottom:10px;font-family:'Yanone Kaffeesatz',sans-serif;} -.box h3{color:#4a757f;font-size:18px;text-align:left;font-weight:normal;font-family:'Yanone Kaffeesatz',sans-serif;padding-left:0px;} -.box .contributorback{background:#eceeeb url(../images/contributorsback.png) no-repeat center left;} -.box label{color:#707070;font-size:15px;display:block;float:right;text-align:left;font-family:'Yanone Kaffeesatz',sans-serif;width:80px;margin-right:18px;} -.box #displayTagFilterControl label{width:160px;} -.box ul{margin-left:22px;} -.box li{list-style-type:disc;font-size:13px;line-height:20px;margin-bottom:10px;color:#707070;} -.box ul.tags{list-style:none;margin:0;padding:0;line-height:170%;display:block;} -.box #displayTagFilterControl p label{color:#707070;font-size:15px;} -.box .inputs #interestingTagInput,.box .inputs #ignoredTagInput{width:153px;padding-left:5px;border:#c9c9b5 1px solid;height:25px;} -.box .inputs #interestingTagAdd,.box .inputs #ignoredTagAdd{background:url(../images/small-button-blue.png) repeat-x top;border:0;color:#4a757f;font-weight:bold;font-size:12px;width:30px;height:27px;margin-top:-2px;cursor:pointer;border-radius:4px;-ms-border-radius:4px;-moz-border-radius:4px;-webkit-border-radius:4px;-khtml-border-radius:4px;text-shadow:0px 1px 0px #e6f6fa;-moz-text-shadow:0px 1px 0px #e6f6fa;-webkit-text-shadow:0px 1px 0px #e6f6fa;-webkit-box-shadow:1px 1px 2px #808080;-moz-box-shadow:1px 1px 2px #808080;box-shadow:1px 1px 2px #808080;} -.box .inputs #interestingTagAdd:hover,.box .inputs #ignoredTagAdd:hover{background:url(../images/small-button-blue.png) repeat-x bottom;} -.box img.gravatar{margin:1px;} -.box a.followed,.box a.follow{background:url(../images/medium-button.png) top repeat-x;height:34px;line-height:34px;text-align:center;border:0;font-family:'Yanone Kaffeesatz',sans-serif;color:#4a757f;font-weight:normal;font-size:21px;margin-top:3px;display:block;width:120px;text-decoration:none;border-radius:4px;-ms-border-radius:4px;-moz-border-radius:4px;-webkit-border-radius:4px;-khtml-border-radius:4px;-webkit-box-shadow:1px 1px 2px #636363;-moz-box-shadow:1px 1px 2px #636363;box-shadow:1px 1px 2px #636363;margin:0 auto;padding:0;} -.box a.followed:hover,.box a.follow:hover{text-decoration:none;background:url(../images/medium-button.png) bottom repeat-x;text-shadow:0px 1px 0px #c6d9dd;-moz-text-shadow:0px 1px 0px #c6d9dd;-webkit-text-shadow:0px 1px 0px #c6d9dd;} -.box a.followed div.unfollow{display:none;} -.box a.followed:hover div{display:none;} -.box a.followed:hover div.unfollow{display:inline;color:#a05736;} -.box .favorite-number{padding:5px 0 0 5px;font-size:100%;font-family:Arial;font-weight:bold;color:#777;text-align:center;} -.box .notify-sidebar #question-subscribe-sidebar{margin:7px 0 0 3px;} -.statsWidget p{color:#707070;font-size:16px;border-bottom:#cccccc 1px solid;font-size:13px;}.statsWidget p strong{float:right;padding-right:10px;} -.questions-related{word-wrap:break-word;}.questions-related p{line-height:20px;padding:4px 0px 4px 0px;font-size:16px;font-weight:normal;border-bottom:#cccccc 1px solid;} -.questions-related a{font-size:13px;} -#tips li{color:#707070;font-size:13px;list-style-image:url(../images/tips.png);} -#tips a{font-size:16px;} -#markdownHelp li{color:#707070;font-size:13px;} -#markdownHelp a{font-size:16px;} -.tabBar{background-color:#eff5f6;height:30px;margin-bottom:3px;margin-top:3px;float:right;font-family:Georgia,serif;font-size:16px;border-radius:5px;-ms-border-radius:5px;-moz-border-radius:5px;-webkit-border-radius:5px;-khtml-border-radius:5px;} -.tabBar h2{float:left;} -.tabsA,.tabsC{float:right;position:relative;display:block;height:20px;} -.tabsA{float:right;} -.tabsC{float:left;} -.tabsA a,.tabsC a{border-left:1px solid #d0e1e4;color:#7ea9b3;display:block;float:left;height:20px;line-height:20px;padding:4px 7px 4px 7px;text-decoration:none;} -.tabsA a.on,.tabsC a.on,.tabsA a:hover,.tabsC a:hover{color:#4a757f;} -.tabsA .label,.tabsC .label{float:left;color:#646464;margin-top:4px;margin-right:5px;} -.main-page .tabsA .label{margin-left:8px;} -.tabsB a{background:#eee;border:1px solid #eee;color:#777;display:block;float:left;height:22px;line-height:28px;margin:5px 0px 0 4px;padding:0 11px 0 11px;text-decoration:none;} -.tabsC .first{border:none;} -.rss{float:right;font-size:16px;color:#f57900;margin:5px 0px 3px 7px;width:52px;padding-left:2px;padding-top:3px;background:#ffffff url(../images/feed-icon-small.png) no-repeat center right;float:right;font-family:Georgia,serif;font-size:16px;} -.rss:hover{color:#F4A731 !important;} -#questionCount{font-weight:bold;font-size:23px;color:#7ea9b3;width:200px;float:left;margin-bottom:8px;padding-top:6px;font-family:'Yanone Kaffeesatz',sans-serif;} -#listSearchTags{float:left;margin-top:3px;color:#707070;font-size:16px;font-family:'Yanone Kaffeesatz',sans-serif;} -ul#searchTags{margin-left:10px;float:right;padding-top:2px;} -.search-tips{font-size:16px;line-height:17px;color:#707070;margin:5px 0 10px 0;padding:0px;float:left;font-family:'Yanone Kaffeesatz',sans-serif;}.search-tips a{text-decoration:underline;color:#1b79bd;} -#question-list{float:left;position:relative;background-color:#FFF;padding:0;width:100%;} -.short-summary{position:relative;filter:inherit;padding:10px;border-bottom:1px solid #DDDBCE;margin-bottom:1px;overflow:hidden;width:710px;float:left;background:url(../images/summary-background.png) repeat-x;}.short-summary h2{font-size:24px;font-weight:normal;line-height:26px;padding-left:0;margin-bottom:6px;display:block;font-family:'Yanone Kaffeesatz',sans-serif;} -.short-summary a{color:#464646;} -.short-summary .userinfo{text-align:right;line-height:16px;font-family:Arial;padding-right:4px;} -.short-summary .userinfo .relativetime,.short-summary span.anonymous{font-size:11px;clear:both;font-weight:normal;color:#555;} -.short-summary .userinfo a{font-weight:bold;font-size:11px;} -.short-summary .counts{float:right;margin:4px 0 0 5px;font-family:'Yanone Kaffeesatz',sans-serif;} -.short-summary .counts .item-count{padding:0px 5px 0px 5px;font-size:25px;font-family:'Yanone Kaffeesatz',sans-serif;} -.short-summary .counts .votes div,.short-summary .counts .views div,.short-summary .counts .answers div,.short-summary .counts .favorites div{margin-top:3px;font-size:14px;line-height:14px;color:#646464;} -.short-summary .tags{margin-top:0;} -.short-summary .votes,.short-summary .answers,.short-summary .favorites,.short-summary .views{text-align:center;margin:0 3px;padding:8px 2px 0px 2px;width:51px;float:right;height:44px;border:#dbdbd4 1px solid;} -.short-summary .votes{background:url(../images/vote-background.png) repeat-x;} -.short-summary .answers{background:url(../images/answers-background.png) repeat-x;} -.short-summary .views{background:url(../images/view-background.png) repeat-x;} -.short-summary .no-votes .item-count{color:#b1b5b6;} -.short-summary .some-votes .item-count{color:#4a757f;} -.short-summary .no-answers .item-count{color:#b1b5b6;} -.short-summary .some-answers .item-count{color:#eab243;} -.short-summary .no-views .item-count{color:#b1b5b6;} -.short-summary .some-views .item-count{color:#d33f00;} -.short-summary .accepted .item-count{background:url(../images/accept.png) no-repeat top right;display:block;text-align:center;width:40px;color:#eab243;} -.short-summary .some-favorites .item-count{background:#338333;color:#d0f5a9;} -.short-summary .no-favorites .item-count{background:#eab243;color:yellow;} -.evenMore{font-size:13px;color:#707070;padding:15px 0px 10px 0px;clear:both;} -.evenMore a{text-decoration:underline;color:#1b79bd;} -.pager{margin-top:10px;margin-bottom:16px;} -.pagesize{margin-top:10px;margin-bottom:16px;float:right;} -.paginator{padding:5px 0 10px 0;font-size:13px;margin-bottom:10px;}.paginator .prev a,.paginator .prev a:visited,.paginator .next a,.paginator .next a:visited{background-color:#fff;color:#777;padding:2px 4px 3px 4px;} -.paginator a{color:#7ea9b3;} -.paginator .prev{margin-right:.5em;} -.paginator .next{margin-left:.5em;} -.paginator .page a,.paginator .page a:visited,.paginator .curr{padding:.25em;background-color:#fff;margin:0em .25em;color:#ff;} -.paginator .curr{background-color:#8ebcc7;color:#fff;font-weight:bold;} -.paginator .next a,.paginator .prev a{color:#7ea9b3;} -.paginator .page a:hover,.paginator .curr a:hover,.paginator .prev a:hover,.paginator .next a:hover{color:#8C8C8C;background-color:#E1E1E1;text-decoration:none;} -.paginator .text{color:#777;padding:.3em;} -.paginator .paginator-container-left{padding:5px 0 10px 0;} -.tag-size-1{font-size:12px;} -.tag-size-2{font-size:13px;} -.tag-size-3{font-size:14px;} -.tag-size-4{font-size:15px;} -.tag-size-5{font-size:16px;} -.tag-size-6{font-size:17px;} -.tag-size-7{font-size:18px;} -.tag-size-8{font-size:19px;} -.tag-size-9{font-size:20px;} -.tag-size-10{font-size:21px;} -ul.tags,ul.tags.marked-tags,ul#related-tags{list-style:none;margin:0;padding:0;line-height:170%;display:block;} -ul.tags li{float:left;display:block;margin:0 8px 0 0;padding:0;height:20px;} -.wildcard-tags{clear:both;} -ul.tags.marked-tags li,.wildcard-tags ul.tags li{margin-bottom:5px;} -#tagSelector div.inputs{clear:both;float:none;margin-bottom:10px;} -.tags-page ul.tags li,ul#ab-user-tags li{width:160px;margin:5px;} -ul#related-tags li{margin:0 5px 8px 0;float:left;clear:left;} -.tag-left{cursor:pointer;display:block;float:left;height:17px;margin:0 5px 0 0;padding:0;-webkit-box-shadow:0px 0px 5px #d3d6d7;-moz-box-shadow:0px 0px 5px #d3d6d7;box-shadow:0px 0px 5px #d3d6d7;} -.tag-right{background:#f3f6f6;border:#fff 1px solid ;border-top:#fff 2px solid;outline:#cfdbdb 1px solid;display:block;float:left;height:17px;line-height:17px;font-weight:normal;font-size:11px;padding:0px 8px 0px 8px;text-decoration:none;text-align:center;white-space:nowrap;vertical-align:middle;font-family:Arial;color:#717179;} -.deletable-tag{margin-right:3px;white-space:nowrap;border-top-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px;-webkit-border-bottom-right-radius:4px;-webkit-border-top-right-radius:4px;} -.tags a.tag-right,.tags span.tag-right{color:#585858;text-decoration:none;} -.tags a:hover{color:#1A1A1A;} -.users-page h1,.tags-page h1{float:left;} -.main-page h1{margin-right:5px;} -.delete-icon{margin-top:-1px;float:left;height:21px;width:18px;display:block;line-height:20px;text-align:center;background:#bbcdcd;cursor:default;color:#fff;border-top:#cfdbdb 1px solid;font-family:Arial;border-top-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px;-webkit-border-bottom-right-radius:4px;-webkit-border-top-right-radius:4px;text-shadow:0px 1px 0px #7ea0a0;-moz-text-shadow:0px 1px 0px #7ea0a0;-webkit-text-shadow:0px 1px 0px #7ea0a0;} -.delete-icon:hover{background:#b32f2f;} -.tag-number{font-weight:normal;float:left;font-size:16px;color:#5d5d5d;} -.badges .tag-number{float:none;display:inline;padding-right:15px;} -.section-title{color:#7ea9b3;font-family:'Yanone Kaffeesatz',sans-serif;font-weight:bold;font-size:24px;} -#fmask{margin-bottom:30px;width:100%;} -#askFormBar{display:inline-block;padding:4px 7px 5px 0px;margin-top:0px;}#askFormBar p{margin:0 0 5px 0;font-size:14px;color:#525252;line-height:1.4;} -#askFormBar .questionTitleInput{font-size:24px;line-height:24px;height:36px;margin:0px;padding:0px 0 0 5px;border:#cce6ec 3px solid;width:725px;} -.ask-page div#question-list,.edit-question-page div#question-list{float:none;border-bottom:#f0f0ec 1px solid;float:left;margin-bottom:10px;}.ask-page div#question-list a,.edit-question-page div#question-list a{line-height:30px;} -.ask-page div#question-list h2,.edit-question-page div#question-list h2{font-size:13px;padding-bottom:0;color:#1b79bd;border-top:#f0f0ec 1px solid;border-left:#f0f0ec 1px solid;height:30px;line-height:30px;font-weight:normal;} -.ask-page div#question-list span,.edit-question-page div#question-list span{width:28px;height:26px;line-height:26px;text-align:center;margin-right:10px;float:left;display:block;color:#fff;background:#b8d0d5;border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;-khtml-border-radius:3px;} -.ask-page label,.edit-question-page label{color:#525252;font-size:13px;} -.ask-page #id_tags,.edit-question-page #id_tags{border:#cce6ec 3px solid;height:25px;padding-left:5px;width:395px;font-size:14px;} -.title-desc{color:#707070;font-size:13px;} -#fmanswer input.submit,.ask-page input.submit,.edit-question-page input.submit{float:left;background:url(../images/medium-button.png) top repeat-x;height:34px;border:0;font-family:'Yanone Kaffeesatz',sans-serif;color:#4a757f;font-weight:normal;font-size:21px;margin-top:3px;border-radius:4px;-ms-border-radius:4px;-moz-border-radius:4px;-webkit-border-radius:4px;-khtml-border-radius:4px;-webkit-box-shadow:1px 1px 2px #636363;-moz-box-shadow:1px 1px 2px #636363;box-shadow:1px 1px 2px #636363;margin-right:7px;} -#fmanswer input.submit:hover,.ask-page input.submit:hover,.edit-question-page input.submit:hover{text-decoration:none;background:url(../images/medium-button.png) bottom repeat-x;text-shadow:0px 1px 0px #c6d9dd;-moz-text-shadow:0px 1px 0px #c6d9dd;-webkit-text-shadow:0px 1px 0px #c6d9dd;} -#editor{font-size:100%;min-height:200px;line-height:18px;margin:0;border-left:#cce6ec 3px solid;border-bottom:#cce6ec 3px solid;border-right:#cce6ec 3px solid;border-top:0;padding:10px;margin-bottom:10px;width:710px;} -#id_title{width:100%;} -.wmd-preview{margin:3px 0 5px 0;padding:6px;background-color:#F5F5F5;min-height:20px;overflow:auto;font-size:13px;font-family:Arial;}.wmd-preview p{margin-bottom:14px;line-height:1.4;font-size:14px;} -.wmd-preview pre{background-color:#E7F1F8;} -.wmd-preview blockquote{background-color:#eee;} -.wmd-preview IMG{max-width:600px;} -.preview-toggle{width:100%;color:#b6a475;text-align:left;} -.preview-toggle span:hover{cursor:pointer;} -.after-editor{margin-top:15px;margin-bottom:15px;} -.checkbox{margin-left:5px;font-weight:normal;cursor:help;} -.question-options{margin-top:1px;color:#666;line-height:13px;margin-bottom:5px;} -.question-options label{vertical-align:text-bottom;} -.edit-content-html{border-top:1px dotted #D8D2A9;border-bottom:1px dotted #D8D2A9;margin:5px 0 5px 0;} -.edit-question-page,#fmedit,.wmd-preview{color:#525252;}.edit-question-page #id_revision,#fmedit #id_revision,.wmd-preview #id_revision{font-size:14px;margin-top:5px;margin-bottom:5px;} -.edit-question-page #id_title,#fmedit #id_title,.wmd-preview #id_title{font-size:24px;line-height:24px;height:36px;margin:0px;padding:0px 0 0 5px;border:#cce6ec 3px solid;width:725px;margin-bottom:10px;} -.edit-question-page #id_summary,#fmedit #id_summary,.wmd-preview #id_summary{border:#cce6ec 3px solid;height:25px;padding-left:5px;width:395px;font-size:14px;} -.edit-question-page .title-desc,#fmedit .title-desc,.wmd-preview .title-desc{margin-bottom:10px;} -.question-page h1{padding-top:0px;font-family:'Yanone Kaffeesatz',sans-serif;} -.question-page h1 a{color:#464646;font-size:30px;font-weight:normal;line-height:1;} -.question-page p.rss{float:none;clear:both;padding:3px 0 0 23px;font-size:15px;width:110px;background-position:center left;margin-left:0px !important;} -.question-page p.rss a{font-family:'Yanone Kaffeesatz',sans-serif;vertical-align:top;} -.question-page .question-content{float:right;width:682px;margin-bottom:10px;} -.question-page #question-table{float:left;border-top:#f0f0f0 1px solid;} -.question-page #question-table,.question-page .answer-table{margin:6px 0 6px 0;border-spacing:0px;width:670px;padding-right:10px;} -.question-page .answer-table{margin-top:0px;border-bottom:1px solid #D4D4D4;float:right;} -.question-page .answer-table td,.question-page #question-table td{width:20px;vertical-align:top;} -.question-page .question-body,.question-page .answer-body{overflow:auto;margin-top:10px;font-family:Arial;color:#4b4b4b;}.question-page .question-body p,.question-page .answer-body p{margin-bottom:14px;line-height:1.4;font-size:14px;padding:0px 5px 5px 0px;} -.question-page .question-body a,.question-page .answer-body a{color:#1b79bd;} -.question-page .question-body li,.question-page .answer-body li{margin-bottom:7px;} -.question-page .question-body IMG,.question-page .answer-body IMG{max-width:600px;} -.question-page .post-update-info-container{float:right;width:175px;} -.question-page .post-update-info{background:#ffffff url(../images/background-user-info.png) repeat-x bottom;float:right;font-size:9px;font-family:Arial;width:158px;padding:4px;margin:0px 0px 5px 5px;line-height:14px;border-radius:4px;-ms-border-radius:4px;-moz-border-radius:4px;-webkit-border-radius:4px;-khtml-border-radius:4px;-webkit-box-shadow:0px 2px 1px #bfbfbf;-moz-box-shadow:0px 2px 1px #bfbfbf;box-shadow:0px 2px 1px #bfbfbf;}.question-page .post-update-info p{line-height:13px;font-size:11px;margin:0 0 2px 1px;padding:0;} -.question-page .post-update-info a{color:#444;} -.question-page .post-update-info .gravatar{float:left;margin-right:4px;} -.question-page .post-update-info p.tip{color:#444;line-height:13px;font-size:10px;} -.question-page .post-controls{font-size:11px;line-height:12px;min-width:200px;padding-left:5px;text-align:right;clear:left;float:right;margin-top:10px;margin-bottom:8px;}.question-page .post-controls a{color:#777;padding:0px 3px 3px 22px;cursor:pointer;border:none;font-size:12px;font-family:Arial;text-decoration:none;height:18px;display:block;float:right;line-height:18px;margin-top:-2px;margin-left:4px;} -.question-page .post-controls a:hover{background-color:#f5f0c9;border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;-khtml-border-radius:3px;} -.question-page .post-controls .sep{color:#ccc;float:right;height:18px;font-size:18px;} -.question-page .post-controls .question-delete,.question-page .answer-controls .question-delete{background:url(../images/delete.png) no-repeat center left;padding-left:16px;} -.question-page .post-controls .question-flag,.question-page .answer-controls .question-flag{background:url(../images/flag.png) no-repeat center left;} -.question-page .post-controls .question-edit,.question-page .answer-controls .question-edit{background:url(../images/edit2.png) no-repeat center left;} -.question-page .post-controls .question-retag,.question-page .answer-controls .question-retag{background:url(../images/retag.png) no-repeat center left;} -.question-page .post-controls .question-close,.question-page .answer-controls .question-close{background:url(../images/close.png) no-repeat center left;} -.question-page .post-controls .permant-link,.question-page .answer-controls .permant-link{background:url(../images/link.png) no-repeat center left;} -.question-page .tabBar{width:100%;} -.question-page #questionCount{float:left;font-family:'Yanone Kaffeesatz',sans-serif;line-height:15px;} -.question-page .question-img-upvote,.question-page .question-img-downvote,.question-page .answer-img-upvote,.question-page .answer-img-downvote{width:25px;height:20px;cursor:pointer;} -.question-page .question-img-upvote,.question-page .answer-img-upvote{background:url(../images/vote-arrow-up-new.png) no-repeat;} -.question-page .question-img-downvote,.question-page .answer-img-downvote{background:url(../images/vote-arrow-down-new.png) no-repeat;} -.question-page .question-img-upvote:hover,.question-page .question-img-upvote.on,.question-page .answer-img-upvote:hover,.question-page .answer-img-upvote.on{background:url(../images/vote-arrow-up-on-new.png) no-repeat;} -.question-page .question-img-downvote:hover,.question-page .question-img-downvote.on,.question-page .answer-img-downvote:hover,.question-page .answer-img-downvote.on{background:url(../images/vote-arrow-down-on-new.png) no-repeat;} -.question-page #fmanswer_button{margin:8px 0px ;} -.question-page .question-img-favorite:hover{background:url(../images/vote-favorite-on.png);} -.question-page div.comments{padding:0;} -.question-page #comment-title{font-weight:bold;font-size:23px;color:#7ea9b3;width:200px;float:left;font-family:'Yanone Kaffeesatz',sans-serif;} -.question-page .comments{font-size:12px;clear:both;}.question-page .comments div.controls{clear:both;float:left;width:100%;margin:3px 0 20px 5px;} -.question-page .comments .controls a{color:#988e4c;padding:0 3px 2px 22px;font-family:Arial;font-size:13px;background:url(../images/comment.png) no-repeat center left;} -.question-page .comments .controls a:hover{background-color:#f5f0c9;text-decoration:none;} -.question-page .comments .button{color:#988e4c;font-size:11px;padding:3px;cursor:pointer;} -.question-page .comments a{background-color:inherit;color:#1b79bd;padding:0;} -.question-page .comments form.post-comments{margin:3px 26px 0 42px;}.question-page .comments form.post-comments textarea{font-size:13px;line-height:1.3;} -.question-page .comments textarea{height:42px;width:100%;margin:7px 0 5px 1px;font-family:Arial;outline:none;overflow:auto;font-size:12px;line-height:140%;padding-left:2px;padding-top:3px;border:#cce6ec 3px solid;} -@media screen and (-webkit-min-device-pixel-ratio:0){textarea{padding-left:3px !important;}}.question-page .comments input{margin-left:10px;margin-top:1px;vertical-align:top;width:100px;} -.question-page .comments button{background:url(../images/small-button-blue.png) repeat-x top;border:0;color:#4a757f;font-family:Arial;font-size:13px;width:100px;font-weight:bold;height:27px;line-height:25px;margin-bottom:5px;cursor:pointer;border-radius:4px;-ms-border-radius:4px;-moz-border-radius:4px;-webkit-border-radius:4px;-khtml-border-radius:4px;text-shadow:0px 1px 0px #e6f6fa;-moz-text-shadow:0px 1px 0px #e6f6fa;-webkit-text-shadow:0px 1px 0px #e6f6fa;-webkit-box-shadow:1px 1px 2px #808080;-moz-box-shadow:1px 1px 2px #808080;box-shadow:1px 1px 2px #808080;} -.question-page .comments button:hover{background:url(../images/small-button-blue.png) bottom repeat-x;text-shadow:0px 1px 0px #c6d9dd;-moz-text-shadow:0px 1px 0px #c6d9dd;-webkit-text-shadow:0px 1px 0px #c6d9dd;} -.question-page .comments .counter{display:inline-block;width:245px;float:right;color:#b6a475 !important;vertical-align:top;font-family:Arial;float:right;text-align:right;} -.question-page .comments .comment{border-bottom:1px solid #edeeeb;clear:both;margin:0;margin-top:8px;padding-bottom:4px;overflow:auto;font-family:Arial;font-size:11px;min-height:25px;background:#ffffff url(../images/comment-background.png) bottom repeat-x;border-radius:5px;-ms-border-radius:5px;-moz-border-radius:5px;-webkit-border-radius:5px;-khtml-border-radius:5px;} -.question-page .comments div.comment:hover{background-color:#efefef;} -.question-page .comments a.author{background-color:inherit;color:#1b79bd;padding:0;} -.question-page .comments a.author:hover{text-decoration:underline;} -.question-page .comments span.delete-icon{background:url(../images/close-small.png) no-repeat;border:0;width:14px;height:14px;} -.question-page .comments span.delete-icon:hover{border:#BC564B 2px solid;border-radius:10px;-ms-border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:10px;-khtml-border-radius:10px;margin:-3px 0px 0px -2px;} -.question-page .comments .content{margin-bottom:7px;} -.question-page .comments .comment-votes{float:left;width:37px;line-height:130%;padding:6px 5px 6px 3px;} -.question-page .comments .comment-body{line-height:1.3;margin:3px 26px 0 46px;padding:5px 3px;color:#666;font-size:13px;}.question-page .comments .comment-body .edit{padding-left:6px;} -.question-page .comments .comment-body p{font-size:13px;line-height:1.3;margin-bottom:3px;padding:0;} -.question-page .comments .comment-delete{float:right;width:14px;line-height:130%;padding:8px 6px;} -.question-page .comments .upvote{margin:0px;padding-right:17px;padding-top:2px;text-align:right;height:20px;font-size:13px;font-weight:bold;color:#777;} -.question-page .comments .upvote.upvoted{color:#d64000;} -.question-page .comments .upvote.hover{background:url(../images/go-up-grey.png) no-repeat;background-position:right 1px;} -.question-page .comments .upvote:hover{background:url(../images/go-up-orange.png) no-repeat;background-position:right 1px;} -.question-page .comments .help-text{float:right;text-align:right;color:gray;margin-bottom:0px;margin-top:0px;line-height:50%;} -.question-page #questionTools{font-size:22px;margin-top:11px;text-align:left;} -.question-page .question-status{margin-top:10px;margin-bottom:15px;padding:20px;background-color:#fef7cc;text-align:center;border:#e1c04a 1px solid;} -.question-page .question-status h3{font-size:20px;color:#707070;font-weight:normal;} -.question-page .vote-buttons{float:left;text-align:center;padding-top:2px;margin:10px 10px 0px 3px;} -.question-page .vote-buttons IMG{cursor:pointer;} -.question-page .vote-number{font-family:'Yanone Kaffeesatz',sans-serif;padding:0px 0 5px 0;font-size:25px;font-weight:bold;color:#777;} -.question-page .vote-buttons .notify-sidebar{text-align:left;width:120px;} -.question-page .vote-buttons .notify-sidebar label{vertical-align:top;} -.question-page .tabBar-answer{margin-bottom:15px;padding-left:7px;width:723px;margin-top:10px;} -.question-page .answer .vote-buttons{float:left;} -.question-page .accepted-answer{background-color:#f7fecc;border-bottom-color:#9BD59B;}.question-page .accepted-answer .vote-buttons{width:27px;margin-right:10px;margin-top:10px;} -.question-page .answer .post-update-info a{color:#444444;} -.question-page .answered{background:#CCC;color:#999;} -.question-page .answered-accepted{background:#DCDCDC;color:#763333;}.question-page .answered-accepted strong{color:#E1E818;} -.question-page .answered-by-owner{background:#F1F1FF;}.question-page .answered-by-owner .comments .button{background-color:#E6ECFF;} -.question-page .answered-by-owner .comments{background-color:#E6ECFF;} -.question-page .answered-by-owner .vote-buttons{margin-right:10px;} -.question-page .answer-img-accept:hover{background:url(../images/vote-accepted-on.png);} -.question-page .answer-body a{color:#1b79bd;} -.question-page .answer-body li{margin-bottom:0.7em;} -.question-page #fmanswer{color:#707070;line-height:1.2;margin-top:10px;}.question-page #fmanswer h2{font-family:'Yanone Kaffeesatz',sans-serif;color:#7ea9b3;font-size:24px;} -.question-page #fmanswer label{font-size:13px;} -.question-page .message{padding:5px;margin:0px 0 10px 0;} -.facebook-share.icon,.twitter-share.icon,.linkedin-share.icon,.identica-share.icon{background:url(../images/socialsprite.png) no-repeat;display:block;text-indent:-100em;height:25px;width:25px;margin-bottom:3px;} -.facebook-share.icon:hover,.twitter-share.icon:hover,.linkedin-share.icon:hover,.identica-share.icon:hover{opacity:0.8;filter:alpha(opacity=80);} -.facebook-share.icon{background-position:-26px 0px;} -.identica-share.icon{background-position:-78px 0px;} -.twitter-share.icon{margin-top:10px;background-position:0px 0px;} -.linkedin-share.icon{background-position:-52px 0px;} -.openid-signin,.meta,.users-page,.user-profile-edit-page{font-size:13px;line-height:1.3;color:#525252;}.openid-signin p,.meta p,.users-page p,.user-profile-edit-page p{font-size:13px;color:#707070;line-height:1.3;font-family:Arial;color:#525252;margin-bottom:12px;} -.openid-signin h2,.meta h2,.users-page h2,.user-profile-edit-page h2{color:#525252;padding-left:0px;font-size:16px;} -.openid-signin form,.meta form,.users-page form,.user-profile-edit-page form,.user-profile-page form{margin-bottom:15px;} -.openid-signin input[type="text"],.meta input[type="text"],.users-page input[type="text"],.user-profile-edit-page input[type="text"],.user-profile-page input[type="text"],.openid-signin input[type="password"],.meta input[type="password"],.users-page input[type="password"],.user-profile-edit-page input[type="password"],.user-profile-page input[type="password"],.openid-signin select,.meta select,.users-page select,.user-profile-edit-page select,.user-profile-page select{border:#cce6ec 3px solid;height:25px;padding-left:5px;width:395px;font-size:14px;} -.openid-signin select,.meta select,.users-page select,.user-profile-edit-page select,.user-profile-page select{width:405px;height:30px;} -.openid-signin textarea,.meta textarea,.users-page textarea,.user-profile-edit-page textarea,.user-profile-page textarea{border:#cce6ec 3px solid;padding-left:5px;padding-top:5px;width:395px;font-size:14px;} -.openid-signin input.submit,.meta input.submit,.users-page input.submit,.user-profile-edit-page input.submit,.user-profile-page input.submit{background:url(../images/small-button-blue.png) repeat-x top;border:0;color:#4a757f;font-weight:bold;font-size:13px;font-family:Arial;height:26px;margin:5px 0px;width:100px;cursor:pointer;border-radius:4px;-ms-border-radius:4px;-moz-border-radius:4px;-webkit-border-radius:4px;-khtml-border-radius:4px;text-shadow:0px 1px 0px #e6f6fa;-moz-text-shadow:0px 1px 0px #e6f6fa;-webkit-text-shadow:0px 1px 0px #e6f6fa;-webkit-box-shadow:1px 1px 2px #808080;-moz-box-shadow:1px 1px 2px #808080;box-shadow:1px 1px 2px #808080;} -.openid-signin input.submit:hover,.meta input.submit:hover,.users-page input.submit:hover,.user-profile-edit-page input.submit:hover,.user-profile-page input.submit:hover{background:url(../images/small-button-blue.png) repeat-x bottom;text-decoration:none;} -.openid-signin .cancel,.meta .cancel,.users-page .cancel,.user-profile-edit-page .cancel,.user-profile-page .cancel{background:url(../images/small-button-cancel.png) repeat-x top !important;color:#525252 !important;} -.openid-signin .cancel:hover,.meta .cancel:hover,.users-page .cancel:hover,.user-profile-edit-page .cancel:hover,.user-profile-page .cancel:hover{background:url(../images/small-button-cancel.png) repeat-x bottom !important;} -#email-input-fs,#local_login_buttons,#password-fs,#openid-fs{margin-top:10px;}#email-input-fs #id_email,#local_login_buttons #id_email,#password-fs #id_email,#openid-fs #id_email,#email-input-fs #id_username,#local_login_buttons #id_username,#password-fs #id_username,#openid-fs #id_username,#email-input-fs #id_password,#local_login_buttons #id_password,#password-fs #id_password,#openid-fs #id_password{font-size:12px;line-height:20px;height:20px;margin:0px;padding:0px 0 0 5px;border:#cce6ec 3px solid;width:200px;} -#email-input-fs .submit-b,#local_login_buttons .submit-b,#password-fs .submit-b,#openid-fs .submit-b{background:url(../images/small-button-blue.png) repeat-x top;border:0;color:#4a757f;font-weight:bold;font-size:13px;font-family:Arial;height:24px;margin-top:-2px;padding-left:10px;padding-right:10px;cursor:pointer;border-radius:4px;-ms-border-radius:4px;-moz-border-radius:4px;-webkit-border-radius:4px;-khtml-border-radius:4px;text-shadow:0px 1px 0px #e6f6fa;-moz-text-shadow:0px 1px 0px #e6f6fa;-webkit-text-shadow:0px 1px 0px #e6f6fa;-webkit-box-shadow:1px 1px 2px #808080;-moz-box-shadow:1px 1px 2px #808080;box-shadow:1px 1px 2px #808080;} -#email-input-fs .submit-b:hover,#local_login_buttons .submit-b:hover,#password-fs .submit-b:hover,#openid-fs .submit-b:hover{background:url(../images/small-button-blue.png) repeat-x bottom;} -.openid-input{background:url(../images/openid.gif) no-repeat;padding-left:15px;cursor:pointer;} -.openid-login-input{background-position:center left;background:url(../images/openid.gif) no-repeat 0% 50%;padding:5px 5px 5px 15px;cursor:pointer;font-family:Trebuchet MS;font-weight:300;font-size:150%;width:500px;} -.openid-login-submit{height:40px;width:80px;line-height:40px;cursor:pointer;border:1px solid #777;font-weight:bold;font-size:120%;} -.tabBar-user{width:375px;} -.user{padding:5px;line-height:140%;width:166px;border:#eee 1px solid;margin-bottom:5px;border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;-khtml-border-radius:3px;}.user .user-micro-info{color:#525252;} -.user ul{margin:0;list-style-type:none;} -.user .thumb{clear:both;float:left;margin-right:4px;display:inline;} -.tabBar-tags{width:270px;margin-bottom:15px;} -a.medal{font-size:17px;line-height:250%;margin-right:5px;color:#333;text-decoration:none;background:url(../images/medala.gif) no-repeat;border-left:1px solid #EEE;border-top:1px solid #EEE;border-bottom:1px solid #CCC;border-right:1px solid #CCC;padding:4px 12px 4px 6px;} -a:hover.medal{color:#333;text-decoration:none;background:url(../images/medala_on.gif) no-repeat;border-left:1px solid #E7E296;border-top:1px solid #E7E296;border-bottom:1px solid #D1CA3D;border-right:1px solid #D1CA3D;} -#award-list .user{float:left;margin:5px;} -.tabBar-profile{width:100%;margin-bottom:15px;float:left;} -.user-profile-page{font-size:13px;color:#525252;}.user-profile-page p{font-size:13px;line-height:1.3;color:#525252;} -.user-profile-page .avatar img{border:#eee 1px solid;padding:5px;} -.user-profile-page h2{padding:10px 0px 10px 0px;font-family:'Yanone Kaffeesatz',sans-serif;} -.user-details{font-size:13px;}.user-details h3{font-size:16px;} -.user-about{background-color:#EEEEEE;height:200px;line-height:20px;overflow:auto;padding:10px;width:90%;}.user-about p{font-size:13px;} -.follow-toggle,.submit{border:0 !important;color:#4a757f;font-weight:bold;font-size:12px;height:26px;line-height:26px;margin-top:-2px;font-size:15px;cursor:pointer;font-family:'Yanone Kaffeesatz',sans-serif;background:url(../images/small-button-blue.png) repeat-x top;border-radius:4px;-ms-border-radius:4px;-moz-border-radius:4px;-webkit-border-radius:4px;-khtml-border-radius:4px;text-shadow:0px 1px 0px #e6f6fa;-moz-text-shadow:0px 1px 0px #e6f6fa;-webkit-text-shadow:0px 1px 0px #e6f6fa;-webkit-box-shadow:1px 1px 2px #808080;-moz-box-shadow:1px 1px 2px #808080;box-shadow:1px 1px 2px #808080;} -.follow-toggle:hover,.submit:hover{background:url(../images/small-button-blue.png) repeat-x bottom;text-decoration:none !important;} -.follow-toggle .follow{font-color:#000;font-style:normal;} -.follow-toggle .unfollow div.unfollow-red{display:none;} -.follow-toggle .unfollow:hover div.unfollow-red{display:inline;color:#fff;font-weight:bold;color:#A05736;} -.follow-toggle .unfollow:hover div.unfollow-green{display:none;} -.count{font-family:'Yanone Kaffeesatz',sans-serif;font-size:200%;font-weight:700;color:#777777;} -.scoreNumber{font-family:'Yanone Kaffeesatz',sans-serif;font-size:35px;font-weight:800;color:#777;line-height:40px;margin-top:3px;} -.vote-count{font-family:Arial;font-size:160%;font-weight:700;color:#777;} -.answer-summary{display:block;clear:both;padding:3px;} -.answer-votes{background-color:#EEEEEE;color:#555555;float:left;font-family:Arial;font-size:15px;font-weight:bold;height:17px;padding:2px 4px 5px;text-align:center;text-decoration:none;width:20px;margin-right:10px;border-radius:4px;-ms-border-radius:4px;-moz-border-radius:4px;-webkit-border-radius:4px;-khtml-border-radius:4px;} -.karma-summary{padding:5px;font-size:13px;} -.karma-summary h3{text-align:center;font-weight:bold;padding:5px;} -.karma-diagram{width:477px;height:300px;float:left;margin-right:10px;} -.karma-details{float:right;width:450px;height:250px;overflow-y:auto;word-wrap:break-word;}.karma-details p{margin-bottom:10px;} -.karma-gained{font-weight:bold;background:#eee;width:25px;margin-right:5px;color:green;padding:3px;display:block;float:left;text-align:center;border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;-khtml-border-radius:3px;} -.karma-lost{font-weight:bold;background:#eee;width:25px;color:red;padding:3px;display:block;margin-right:5px;float:left;text-align:center;border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;-khtml-border-radius:3px;} -.submit-row{margin-bottom:10px;} -.revision{margin:10px 0 10px 0;font-size:13px;color:#525252;}.revision p{font-size:13px;line-height:1.3;color:#525252;} -.revision h3{font-family:'Yanone Kaffeesatz',sans-serif;font-size:21px;padding-left:0px;} -.revision .header{background-color:#F5F5F5;padding:5px;cursor:pointer;} -.revision .author{background-color:#e9f3f5;} -.revision .summary{padding:5px 0 10px 0;} -.revision .summary span{background-color:#fde785;padding:6px;border-radius:4px;-ms-border-radius:4px;-moz-border-radius:4px;-webkit-border-radius:4px;-khtml-border-radius:4px;display:inline;-webkit-box-shadow:1px 1px 4px #cfb852;-moz-box-shadow:1px 1px 4px #cfb852;box-shadow:1px 1px 4px #cfb852;} -.revision .answerbody{padding:10px 0 5px 10px;} -.revision .revision-mark{width:150px;text-align:left;display:inline-block;font-size:11px;overflow:hidden;}.revision .revision-mark .gravatar{float:left;margin-right:4px;padding-top:5px;} -.revision .revision-number{font-size:300%;font-weight:bold;font-family:sans-serif;} -del,del .post-tag{color:#C34719;} -ins .post-tag,ins p,ins{background-color:#E6F0A2;} -.vote-notification{z-index:1;cursor:pointer;display:none;position:absolute;font-family:Arial;font-size:14px;font-weight:normal;color:white;background-color:#8e0000;text-align:center;padding-bottom:10px;-webkit-box-shadow:0px 2px 4px #370000;-moz-box-shadow:0px 2px 4px #370000;box-shadow:0px 2px 4px #370000;border-radius:4px;-ms-border-radius:4px;-moz-border-radius:4px;-webkit-border-radius:4px;-khtml-border-radius:4px;}.vote-notification h3{background:url(../images/notification.png) repeat-x top;padding:10px 10px 10px 10px;font-size:13px;margin-bottom:5px;border-top:#8e0000 1px solid;color:#fff;font-weight:normal;border-top-right-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px;-webkit-border-top-left-radius:4px;-webkit-border-top-right-radius:4px;} -.vote-notification a{color:#fb7321;text-decoration:underline;font-weight:bold;} -#ground{width:100%;clear:both;border-top:1px solid #000;padding:6px 0 0 0;background:#16160f;font-size:16px;font-family:'Yanone Kaffeesatz',sans-serif;}#ground p{margin-bottom:0;} -.footer-links{color:#EEE;text-align:left;width:500px;float:left;}.footer-links a{color:#e7e8a8;} -.powered-link{width:500px;float:left;text-align:left;}.powered-link a{color:#8ebcc7;} -.copyright{color:#616161;width:450px;float:right;text-align:right;}.copyright a{color:#8ebcc7;} -.copyright img.license-logo{margin:6px 0px 20px 10px;float:right;} -.notify-me{float:left;} -span.text-counter{margin-right:20px;} -span.form-error{color:#990000;font-weight:normal;margin-left:5px;} -p.form-item{margin:0px;} -.deleted{background:#F4E7E7 none repeat scroll 0 0;} -.form-row{line-height:25px;} -table.form-as-table{margin-top:5px;} -table.form-as-table ul{list-style-type:none;display:inline;} -table.form-as-table li{display:inline;} -table.form-as-table td{text-align:right;} -table.form-as-table th{text-align:left;font-weight:normal;} -table.ab-subscr-form{width:45em;} -table.ab-tag-filter-form{width:45em;} -.submit-row{line-height:30px;padding-top:10px;display:block;clear:both;} -.errors{line-height:20px;color:red;} -.error{color:darkred;margin:0;font-size:10px;} -label.retag-error{color:darkred;padding-left:5px;font-size:10px;} -.fieldset{border:none;margin-top:10px;padding:10px;} -span.form-error{color:#990000;font-size:90%;font-weight:normal;margin-left:5px;} -.favorites-empty{width:32px;height:45px;float:left;} -.user-info-table{margin-bottom:10px;border-spacing:0;} -.user-stats-table .narrow{width:660px;} -.narrow .summary h3{padding:0px;margin:0px;} -.relativetime{font-weight:bold;text-decoration:none;} -.narrow .tags{float:left;} -.user-action-1{font-weight:bold;color:#333;} -.user-action-2{font-weight:bold;color:#CCC;} -.user-action-3{color:#333;} -.user-action-4{color:#333;} -.user-action-5{color:darkred;} -.user-action-6{color:darkred;} -.user-action-7{color:#333;} -.user-action-8{padding:3px;font-weight:bold;background-color:#CCC;color:#763333;} -.revision-summary{background-color:#FFFE9B;padding:2px;} -.question-title-link a{font-weight:bold;color:#0077CC;} -.answer-title-link a{color:#333;} -.post-type-1 a{font-weight:bold;} -.post-type-3 a{font-weight:bold;} -.post-type-5 a{font-weight:bold;} -.post-type-2 a{color:#333;} -.post-type-4 a{color:#333;} -.post-type-6 a{color:#333;} -.post-type-8 a{color:#333;} -.hilite{background-color:#ff0;} -.hilite1{background-color:#ff0;} -.hilite2{background-color:#f0f;} -.hilite3{background-color:#0ff;} -.gold,.badge1{color:#FFCC00;} -.silver,.badge2{color:#CCCCCC;} -.bronze,.badge3{color:#CC9933;} -.score{font-weight:800;color:#333;} -a.comment{background:#EEE;color:#993300;padding:5px;} -a.offensive{color:#999;} -.message h1{padding-top:0px;font-size:15px;} -.message p{margin-bottom:0px;} -p.space-above{margin-top:10px;} -.warning{color:red;} -button::-moz-focus-inner{padding:0;border:none;} -.submit{cursor:pointer;background-color:#D4D0C8;height:30px;border:1px solid #777777;font-weight:bold;font-size:120%;} -.submit:hover{text-decoration:underline;} -.submit.small{margin-right:5px;height:20px;font-weight:normal;font-size:12px;padding:1px 5px;} -.submit.small:hover{text-decoration:none;} -.question-page a.submit{display:-moz-inline-stack;display:inline-block;line-height:30px;padding:0 5px;*display:inline;} -.noscript{position:fixed;top:0px;left:0px;width:100%;z-index:100;padding:5px 0;text-align:center;font-family:sans-serif;font-size:120%;font-weight:Bold;color:#FFFFFF;background-color:#AE0000;} -.big{font-size:14px;} -.strong{font-weight:bold;} -.orange{color:#d64000;font-weight:bold;} -.grey{color:#808080;} -.about div{padding:10px 5px 10px 5px;border-top:1px dashed #aaaaaa;} -.highlight{background-color:#FFF8C6;} -.nomargin{margin:0;} -.margin-bottom{margin-bottom:10px;} -.margin-top{margin-top:10px;} -.inline-block{display:inline-block;} -.action-status{margin:0;border:none;text-align:center;line-height:10px;font-size:12px;padding:0;} -.action-status span{padding:3px 5px 3px 5px;background-color:#fff380;font-weight:normal;-moz-border-radius:5px;-khtml-border-radius:5px;-webkit-border-radius:5px;} -.list-table td{vertical-align:top;} -table.form-as-table .errorlist{display:block;margin:0;padding:0 0 0 5px;text-align:left;font-size:10px;color:darkred;} -table.form-as-table input{display:inline;margin-left:4px;} -table.form-as-table th{vertical-align:bottom;padding-bottom:4px;} -.form-row-vertical{margin-top:8px;display:block;} -.form-row-vertical label{margin-bottom:3px;display:block;} -.text-align-right{text-align:center;} -ul.form-horizontal-rows{list-style:none;margin:0;} -ul.form-horizontal-rows li{position:relative;height:40px;} -ul.form-horizontal-rows label{display:inline-block;} -ul.form-horizontal-rows ul.errorlist{list-style:none;color:darkred;font-size:10px;line-height:10px;position:absolute;top:2px;left:180px;text-align:left;margin:0;} -ul.form-horizontal-rows ul.errorlist li{height:10px;} -ul.form-horizontal-rows label{position:absolute;left:0px;bottom:6px;margin:0px;line-height:12px;font-size:12px;} -ul.form-horizontal-rows li input{position:absolute;bottom:0px;left:180px;margin:0px;} -.narrow .summary{float:left;} -.user-profile-tool-links{font-weight:bold;vertical-align:top;} -ul.post-tags{margin-left:3px;} -ul.post-tags li{margin-top:4px;margin-bottom:3px;} -ul.post-retag{margin-bottom:0px;margin-left:5px;} -#question-controls .tags{margin:0 0 3px 0;} -#tagSelector{padding-bottom:2px;margin-bottom:0;} -#related-tags{padding-left:3px;} -#hideIgnoredTagsControl{margin:5px 0 0 0;} -#hideIgnoredTagsControl label{font-size:12px;color:#666;} -#hideIgnoredTagsCb{margin:0 2px 0 1px;} -#recaptcha_widget_div{width:318px;float:left;clear:both;} -p.signup_p{margin:20px 0px 0px 0px;} -.simple-subscribe-options ul{list-style:none;list-style-position:outside;margin:0;} -.wmd-preview a{color:#1b79bd;} -.wmd-preview li{margin-bottom:7px;font-size:14px;} -.search-result-summary{font-weight:bold;font-size:18px;line-height:22px;margin:0px 0px 0px 0px;padding:2px 0 0 0;float:left;} -.faq-rep-item{text-align:right;padding-right:5px;} -.user-info-table .gravatar{margin:0;} -#responses{clear:both;line-height:18px;margin-bottom:15px;} -#responses div.face{float:left;text-align:center;width:54px;padding:3px;overflow:hidden;} -.response-parent{margin-top:18px;} -.response-parent strong{font-size:20px;} -.re{min-height:57px;clear:both;margin-top:10px;} -#responses input{float:left;} -#re_tools{margin-bottom:10px;} -#re_sections{margin-bottom:6px;} -#re_sections .on{font-weight:bold;} -.avatar-page ul{list-style:none;} -.avatar-page li{display:inline;} -.user-profile-page .avatar p{margin-bottom:0px;} -.user-profile-page .tabBar a#stats{margin-left:0;} -.user-profile-page img.gravatar{margin:2px 0 3px 0;} -.user-profile-page h3{padding:0;margin-top:-3px;} -.userList{font-size:13px;} -img.flag{border:1px solid #eee;vertical-align:text-top;} -.main-page img.flag{vertical-align:text-bottom;} -a.edit{padding-left:3px;color:#145bff;} -.str{color:#080;} -.kwd{color:#008;} -.com{color:#800;} -.typ{color:#606;} -.lit{color:#066;} -.pun{color:#660;} -.pln{color:#000;} -.tag{color:#008;} -.atn{color:#606;} -.atv{color:#080;} -.dec{color:#606;} -pre.prettyprint{clear:both;padding:3px;border:0px solid #888;} -@media print{.str{color:#060;} .kwd{color:#006;font-weight:bold;} .com{color:#600;font-style:italic;} .typ{color:#404;font-weight:bold;} .lit{color:#044;} .pun{color:#440;} .pln{color:#000;} .tag{color:#006;font-weight:bold;} .atn{color:#404;} .atv{color:#060;}} +/* General Predifined classes, read more in lesscss.org */ +/* Variables for Colors*/ +/* Variables for fonts*/ +/* "Trebuchet MS", sans-serif;*/ +/* Receive exactly positions for background Sprite */ +/* CSS3 Elements */ +/* Library of predifined less functions styles */ +/* ----- General HTML Styles----- */ +body { + background: #FFF; + font-size: 14px; + line-height: 150%; + margin: 0; + padding: 0; + color: #000; + font-family: Arial; +} +div { + margin: 0 auto; + padding: 0; +} +h1, +h2, +h3, +h4, +h5, +h6, +ul, +li, +dl, +dt, +dd, +form, +img, +p { + margin: 0; + padding: 0; + border: none; +} +label { + vertical-align: middle; +} +hr { + border: none; + border-top: 1px dashed #ccccce; +} +input, select { + vertical-align: middle; + font-family: Trebuchet MS, "segoe ui", Helvetica, Tahoma, Verdana, MingLiu, PMingLiu, Arial, sans-serif; + margin-left: 0px; +} +textarea:focus, input:focus { + outline: none; +} +iframe { + border: none; +} +p { + font-size: 14px; + line-height: 140%; + margin-bottom: 6px; +} +a { + color: #1b79bd; + text-decoration: none; + cursor: pointer; +} +h2 { + font-size: 21px; + padding: 3px 0 3px 5px; +} +h3 { + font-size: 19px; + padding: 3px 0 3px 5px; +} +ul { + list-style: disc; + margin-left: 20px; + padding-left: 0px; + margin-bottom: 1em; +} +ol { + list-style: decimal; + margin-left: 30px; + margin-bottom: 1em; + padding-left: 0px; +} +td ul { + vertical-align: middle; +} +li input { + margin: 3px 3px 4px 3px; +} +pre { + font-family: Consolas, Monaco, Liberation Mono, Lucida Console, Monospace; + font-size: 100%; + margin-bottom: 10px; + /*overflow: auto;*/ + + background-color: #F5F5F5; + padding-left: 5px; + padding-top: 5px; + /*width: 671px;*/ + + padding-bottom: 20px ! ie7; +} +code { + font-family: Consolas, Monaco, Liberation Mono, Lucida Console, Monospace; + font-size: 100%; +} +blockquote { + margin-bottom: 10px; + margin-right: 15px; + padding: 10px 0px 1px 10px; + background-color: #F5F5F5; +} +/* http://pathfindersoftware.com/2007/09/developers-note-2/ */ +* html .clearfix, * html .paginator { + height: 1; + overflow: visible; +} ++ html .clearfix, + html .paginator { + min-height: 1%; +} +.clearfix:after, .paginator:after { + clear: both; + content: "."; + display: block; + height: 0; + visibility: hidden; +} +.badges a { + color: #763333; + text-decoration: underline; +} +a:hover { + text-decoration: underline; +} +.badge-context-toggle.active { + cursor: pointer; + text-decoration: underline; +} +h1 { + font-size: 24px; + padding: 10px 0 5px 0px; +} +/* ----- Extra space above for messages ----- */ +body.user-messages { + margin-top: 2.4em; +} +/* ----- Custom positions ----- */ +.left { + float: left; +} +.right { + float: right; +} +.clean { + clear: both; +} +.center { + margin: 0 auto; + padding: 0; +} +/* ----- Notify message bar , check blocks/system_messages.html ----- */ +.notify { + position: fixed; + top: 0px; + left: 0px; + width: 100%; + z-index: 100; + padding: 0; + text-align: center; + background-color: #f5dd69; + border-top: #fff 1px solid; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +.notify p.notification { + margin-top: 6px; + margin-bottom: 6px; + font-size: 16px; + color: #424242; +} +#closeNotify { + position: absolute; + right: 5px; + top: 7px; + color: #735005; + text-decoration: none; + line-height: 18px; + background: -6px -5px url(../images/sprites.png) no-repeat; + cursor: pointer; + width: 20px; + height: 20px; +} +#closeNotify:hover { + background: -26px -5px url(../images/sprites.png) no-repeat; +} +/* ----- Header, check blocks/header.html ----- */ +#header { + margin-top: 0px; + background: #16160f; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +.content-wrapper { + /* wrapper positioning class */ + + width: 960px; + margin: auto; + position: relative; +} +#logo img { + padding: 5px 0px 5px 0px; + height: 75px; + width: auto; + float: left; +} +#userToolsNav { + /* Navigation bar containing login link or user information, check widgets/user_navigation.html*/ + + height: 20px; + padding-bottom: 5px; +} +#userToolsNav a { + height: 35px; + text-align: right; + margin-left: 20px; + text-decoration: underline; + color: #d0e296; + font-size: 16px; +} +#userToolsNav a:first-child { + margin-left: 0; +} +#userToolsNav a#ab-responses { + margin-left: 3px; +} +#userToolsNav .user-info, #userToolsNav .user-micro-info { + color: #b5b593; +} +#userToolsNav a img { + vertical-align: middle; + margin-bottom: 2px; +} +#userToolsNav .user-info a { + margin: 0; + text-decoration: none; +} +#metaNav { + /* Top Navigation bar containing links for tags, people and badges, check widgets/header.html */ + + float: right; + /* for #header.with-logo it is modified */ + +} +#metaNav a { + color: #e2e2ae; + padding: 0px 0px 0px 35px; + height: 25px; + line-height: 30px; + margin: 5px 0px 0px 10px; + font-size: 18px; + font-weight: 100; + text-decoration: none; + display: block; + float: left; +} +#metaNav a:hover { + text-decoration: underline; +} +#metaNav a.on { + font-weight: bold; + color: #FFF; + text-decoration: none; +} +#metaNav a.special { + font-size: 18px; + color: #B02B2C; + font-weight: bold; + text-decoration: none; +} +#metaNav a.special:hover { + text-decoration: underline; +} +#metaNav #navTags { + background: -50px -5px url(../images/sprites.png) no-repeat; +} +#metaNav #navUsers { + background: -125px -5px url(../images/sprites.png) no-repeat; +} +#metaNav #navBadges { + background: -210px -5px url(../images/sprites.png) no-repeat; +} +#header.with-logo #userToolsNav { + position: absolute; + bottom: 0; + right: 0px; +} +#header.without-logo #userToolsNav { + float: left; + margin-top: 7px; +} +#header.without-logo #metaNav { + margin-bottom: 7px; +} +#secondaryHeader { + /* Div containing Home button, scope navigation, search form and ask button, check blocks/secondary_header.html */ + + height: 55px; + background: #e9e9e1; + border-bottom: #d3d3c2 1px solid; + border-top: #fcfcfc 1px solid; + margin-bottom: 10px; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +#secondaryHeader #homeButton { + border-right: #afaf9e 1px solid; + background: -6px -36px url(../images/sprites.png) no-repeat; + height: 55px; + width: 43px; + display: block; + float: left; +} +#secondaryHeader #homeButton:hover { + background: -51px -36px url(../images/sprites.png) no-repeat; +} +#secondaryHeader #scopeWrapper { + width: 688px; + float: left; +} +#secondaryHeader #scopeWrapper a { + display: block; + float: left; +} +#secondaryHeader #scopeWrapper .scope-selector { + font-size: 21px; + color: #5a5a4b; + height: 55px; + line-height: 55px; + margin-left: 24px; +} +#secondaryHeader #scopeWrapper .on { + background: url(../images/scopearrow.png) no-repeat center bottom; +} +#secondaryHeader #scopeWrapper .ask-message { + font-size: 24px; +} +#searchBar { + /* Main search form , check widgets/search_bar.html */ + + display: inline-block; + background-color: #fff; + width: 412px; + border: 1px solid #c9c9b5; + float: right; + height: 42px; + margin: 6px 0px 0px 15px; +} +#searchBar .searchInput, #searchBar .searchInputCancelable { + font-size: 30px; + height: 40px; + font-weight: 300; + background: #FFF; + border: 0px; + color: #484848; + padding-left: 10px; + font-family: Arial; + vertical-align: middle; +} +#searchBar .searchInput { + width: 352px; +} +#searchBar .searchInputCancelable { + width: 317px; +} +#searchBar .logoutsearch { + width: 337px; +} +#searchBar .searchBtn { + font-size: 10px; + color: #666; + background-color: #eee; + height: 42px; + border: #FFF 1px solid; + line-height: 22px; + text-align: center; + float: right; + margin: 0px; + width: 48px; + background: -98px -36px url(../images/sprites.png) no-repeat; + cursor: pointer; +} +#searchBar .searchBtn:hover { + background: -146px -36px url(../images/sprites.png) no-repeat; +} +#searchBar .cancelSearchBtn { + font-size: 30px; + color: #ce8888; + background: #fff; + height: 42px; + border: 0px; + border-left: #deded0 1px solid; + text-align: center; + width: 35px; + cursor: pointer; +} +#searchBar .cancelSearchBtn:hover { + color: #d84040; +} +body.anon #searchBar { + width: 500px; +} +body.anon #searchBar .searchInput { + width: 440px; +} +body.anon #searchBar .searchInputCancelable { + width: 405px; +} +#askButton { + /* check blocks/secondary_header.html and widgets/ask_button.html*/ + + background: url(../images/bigbutton.png) repeat-x bottom; + line-height: 44px; + text-align: center; + width: 200px; + height: 42px; + font-size: 23px; + color: #4a757f; + margin-top: 7px; + float: right; + text-transform: uppercase; + border-radius: 5px; + -ms-border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + -khtml-border-radius: 5px; + -webkit-box-shadow: 1px 1px 2px #636363; + -moz-box-shadow: 1px 1px 2px #636363; + box-shadow: 1px 1px 2px #636363; +} +#askButton:hover { + text-decoration: none; + background: url(../images/bigbutton.png) repeat-x top; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; +} +/* ----- Content layout, check two_column_body.html or one_column_body.html ----- */ +#ContentLeft { + width: 730px; + float: left; + position: relative; + padding-bottom: 10px; +} +#ContentRight { + width: 200px; + float: right; + padding: 0 0px 10px 0px; +} +#ContentFull { + float: left; + width: 960px; +} +/* ----- Sidebar Widgets Box, check main_page/sidebar.html or question/sidebar.html ----- */ +.box { + background: #fff; + padding: 4px 0px 10px 0px; + width: 200px; + /* widgets for question template */ + + /* notify by email box */ + +} +.box p { + margin-bottom: 4px; +} +.box p.info-box-follow-up-links { + text-align: right; + margin: 0; +} +.box h2 { + padding-left: 0; + background: #eceeeb; + height: 30px; + line-height: 30px; + text-align: right; + font-size: 18px !important; + font-weight: normal; + color: #656565; + padding-right: 10px; + margin-bottom: 10px; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +.box h3 { + color: #4a757f; + font-size: 18px; + text-align: left; + font-weight: normal; + font-family: 'Yanone Kaffeesatz', sans-serif; + padding-left: 0px; +} +.box .contributorback { + background: #eceeeb url(../images/contributorsback.png) no-repeat center left; +} +.box label { + color: #707070; + font-size: 15px; + display: block; + float: right; + text-align: left; + font-family: 'Yanone Kaffeesatz', sans-serif; + width: 80px; + margin-right: 18px; +} +.box #displayTagFilterControl label { + /*Especial width just for the display tag filter box in index page*/ + + width: 160px; +} +.box ul { + margin-left: 22px; +} +.box li { + list-style-type: disc; + font-size: 13px; + line-height: 20px; + margin-bottom: 10px; + color: #707070; +} +.box ul.tags { + list-style: none; + margin: 0; + padding: 0; + line-height: 170%; + display: block; +} +.box #displayTagFilterControl p label { + color: #707070; + font-size: 15px; +} +.box .inputs #interestingTagInput, .box .inputs #ignoredTagInput { + width: 153px; + padding-left: 5px; + border: #c9c9b5 1px solid; + height: 25px; +} +.box .inputs #interestingTagAdd, .box .inputs #ignoredTagAdd { + background: url(../images/small-button-blue.png) repeat-x top; + border: 0; + color: #4a757f; + font-weight: bold; + font-size: 12px; + width: 30px; + height: 27px; + margin-top: -2px; + cursor: pointer; + border-radius: 4px; + -ms-border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -khtml-border-radius: 4px; + text-shadow: 0px 1px 0px #e6f6fa; + -moz-text-shadow: 0px 1px 0px #e6f6fa; + -webkit-text-shadow: 0px 1px 0px #e6f6fa; + -webkit-box-shadow: 1px 1px 2px #808080; + -moz-box-shadow: 1px 1px 2px #808080; + box-shadow: 1px 1px 2px #808080; +} +.box .inputs #interestingTagAdd:hover, .box .inputs #ignoredTagAdd:hover { + background: url(../images/small-button-blue.png) repeat-x bottom; +} +.box img.gravatar { + margin: 1px; +} +.box a.followed, .box a.follow { + background: url(../images/medium-button.png) top repeat-x; + height: 34px; + line-height: 34px; + text-align: center; + border: 0; + font-family: 'Yanone Kaffeesatz', sans-serif; + color: #4a757f; + font-weight: normal; + font-size: 21px; + margin-top: 3px; + display: block; + width: 120px; + text-decoration: none; + border-radius: 4px; + -ms-border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -khtml-border-radius: 4px; + -webkit-box-shadow: 1px 1px 2px #636363; + -moz-box-shadow: 1px 1px 2px #636363; + box-shadow: 1px 1px 2px #636363; + margin: 0 auto; + padding: 0; +} +.box a.followed:hover, .box a.follow:hover { + text-decoration: none; + background: url(../images/medium-button.png) bottom repeat-x; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; +} +.box a.followed div.unfollow { + display: none; +} +.box a.followed:hover div { + display: none; +} +.box a.followed:hover div.unfollow { + display: inline; + color: #a05736; +} +.box .favorite-number { + padding: 5px 0 0 5px; + font-size: 100%; + font-family: Arial; + font-weight: bold; + color: #777; + text-align: center; +} +.box .notify-sidebar #question-subscribe-sidebar { + margin: 7px 0 0 3px; +} +.statsWidget p { + color: #707070; + font-size: 16px; + border-bottom: #cccccc 1px solid; + font-size: 13px; +} +.statsWidget p strong { + float: right; + padding-right: 10px; +} +.questions-related { + word-wrap: break-word; +} +.questions-related p { + line-height: 20px; + padding: 4px 0px 4px 0px; + font-size: 16px; + font-weight: normal; + border-bottom: #cccccc 1px solid; +} +.questions-related a { + font-size: 13px; +} +/* tips and markdown help are widgets for ask template */ +#tips li { + color: #707070; + font-size: 13px; + list-style-image: url(../images/tips.png); +} +#tips a { + font-size: 16px; +} +#markdownHelp li { + color: #707070; + font-size: 13px; +} +#markdownHelp a { + font-size: 16px; +} +/* ----- Sorting top Tab, check main_page/tab_bar.html ------*/ +.tabBar { + background-color: #eff5f6; + height: 30px; + margin-bottom: 3px; + margin-top: 3px; + float: right; + font-family: Georgia, serif; + font-size: 16px; + border-radius: 5px; + -ms-border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + -khtml-border-radius: 5px; +} +.tabBar h2 { + float: left; +} +.tabsA, .tabsC { + float: right; + position: relative; + display: block; + height: 20px; +} +/* tabsA - used for sorting */ +.tabsA { + float: right; +} +.tabsC { + float: left; +} +.tabsA a, .tabsC a { + border-left: 1px solid #d0e1e4; + color: #7ea9b3; + display: block; + float: left; + height: 20px; + line-height: 20px; + padding: 4px 7px 4px 7px; + text-decoration: none; +} +.tabsA a.on, +.tabsC a.on, +.tabsA a:hover, +.tabsC a:hover { + color: #4a757f; +} +.tabsA .label, .tabsC .label { + float: left; + color: #646464; + margin-top: 4px; + margin-right: 5px; +} +.main-page .tabsA .label { + margin-left: 8px; +} +.tabsB a { + background: #eee; + border: 1px solid #eee; + color: #777; + display: block; + float: left; + height: 22px; + line-height: 28px; + margin: 5px 0px 0 4px; + padding: 0 11px 0 11px; + text-decoration: none; +} +.tabsC .first { + border: none; +} +.rss { + float: right; + font-size: 16px; + color: #f57900; + margin: 5px 0px 3px 7px; + width: 52px; + padding-left: 2px; + padding-top: 3px; + background: #ffffff url(../images/feed-icon-small.png) no-repeat center right; + float: right; + font-family: Georgia, serif; + font-size: 16px; +} +.rss:hover { + color: #F4A731 !important; +} +/* ----- Headline, containing number of questions and tags selected, check main_page/headline.html ----- */ +#questionCount { + font-weight: bold; + font-size: 23px; + color: #7ea9b3; + width: 200px; + float: left; + margin-bottom: 8px; + padding-top: 6px; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +#listSearchTags { + float: left; + margin-top: 3px; + color: #707070; + font-size: 16px; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +ul#searchTags { + margin-left: 10px; + float: right; + padding-top: 2px; +} +.search-tips { + font-size: 16px; + line-height: 17px; + color: #707070; + margin: 5px 0 10px 0; + padding: 0px; + float: left; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +.search-tips a { + text-decoration: underline; + color: #1b79bd; +} +/* ----- Question list , check main_page/content.html and macros/macros.html----- */ +#question-list { + float: left; + position: relative; + background-color: #FFF; + padding: 0; + width: 100%; +} +.short-summary { + position: relative; + filter: inherit; + padding: 10px; + border-bottom: 1px solid #DDDBCE; + margin-bottom: 1px; + overflow: hidden; + width: 710px; + float: left; + background: url(../images/summary-background.png) repeat-x; +} +.short-summary h2 { + font-size: 24px; + font-weight: normal; + line-height: 26px; + padding-left: 0; + margin-bottom: 6px; + display: block; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +.short-summary a { + color: #464646; +} +.short-summary .userinfo { + text-align: right; + line-height: 16px; + font-family: Arial; + padding-right: 4px; +} +.short-summary .userinfo .relativetime, .short-summary span.anonymous { + font-size: 11px; + clear: both; + font-weight: normal; + color: #555; +} +.short-summary .userinfo a { + font-weight: bold; + font-size: 11px; +} +.short-summary .counts { + float: right; + margin: 4px 0 0 5px; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +.short-summary .counts .item-count { + padding: 0px 5px 0px 5px; + font-size: 25px; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +.short-summary .counts .votes div, +.short-summary .counts .views div, +.short-summary .counts .answers div, +.short-summary .counts .favorites div { + margin-top: 3px; + font-size: 14px; + line-height: 14px; + color: #646464; +} +.short-summary .tags { + margin-top: 0; +} +.short-summary .votes, +.short-summary .answers, +.short-summary .favorites, +.short-summary .views { + text-align: center; + margin: 0 3px; + padding: 8px 2px 0px 2px; + width: 51px; + float: right; + height: 44px; + border: #dbdbd4 1px solid; +} +.short-summary .votes { + background: url(../images/vote-background.png) repeat-x; +} +.short-summary .answers { + background: url(../images/answers-background.png) repeat-x; +} +.short-summary .views { + background: url(../images/view-background.png) repeat-x; +} +.short-summary .no-votes .item-count { + color: #b1b5b6; +} +.short-summary .some-votes .item-count { + color: #4a757f; +} +.short-summary .no-answers .item-count { + color: #b1b5b6; +} +.short-summary .some-answers .item-count { + color: #eab243; +} +.short-summary .no-views .item-count { + color: #b1b5b6; +} +.short-summary .some-views .item-count { + color: #d33f00; +} +.short-summary .accepted .item-count { + background: url(../images/accept.png) no-repeat top right; + display: block; + text-align: center; + width: 40px; + color: #eab243; +} +.short-summary .some-favorites .item-count { + background: #338333; + color: #d0f5a9; +} +.short-summary .no-favorites .item-count { + background: #eab243; + color: yellow; +} +/* ----- Question list Paginator , check main_content/pager.html and macros/utils_macros.html----- */ +.evenMore { + font-size: 13px; + color: #707070; + padding: 15px 0px 10px 0px; + clear: both; +} +.evenMore a { + text-decoration: underline; + color: #1b79bd; +} +.pager { + margin-top: 10px; + margin-bottom: 16px; +} +.pagesize { + margin-top: 10px; + margin-bottom: 16px; + float: right; +} +.paginator { + padding: 5px 0 10px 0; + font-size: 13px; + margin-bottom: 10px; +} +.paginator .prev a, +.paginator .prev a:visited, +.paginator .next a, +.paginator .next a:visited { + background-color: #fff; + color: #777; + padding: 2px 4px 3px 4px; +} +.paginator a { + color: #7ea9b3; +} +.paginator .prev { + margin-right: .5em; +} +.paginator .next { + margin-left: .5em; +} +.paginator .page a, .paginator .page a:visited, .paginator .curr { + padding: .25em; + background-color: #fff; + margin: 0em .25em; + color: #ff; +} +.paginator .curr { + background-color: #8ebcc7; + color: #fff; + font-weight: bold; +} +.paginator .next a, .paginator .prev a { + color: #7ea9b3; +} +.paginator .page a:hover, +.paginator .curr a:hover, +.paginator .prev a:hover, +.paginator .next a:hover { + color: #8C8C8C; + background-color: #E1E1E1; + text-decoration: none; +} +.paginator .text { + color: #777; + padding: .3em; +} +.paginator .paginator-container-left { + padding: 5px 0 10px 0; +} +/* ----- Tags Styles ----- */ +/* tag formatting is also copy-pasted in template + because it must be the same in the emails + askbot/models/__init__.py:format_instant_notification_email() +*/ +/* tag cloud */ +.tag-size-1 { + font-size: 12px; +} +.tag-size-2 { + font-size: 13px; +} +.tag-size-3 { + font-size: 14px; +} +.tag-size-4 { + font-size: 15px; +} +.tag-size-5 { + font-size: 16px; +} +.tag-size-6 { + font-size: 17px; +} +.tag-size-7 { + font-size: 18px; +} +.tag-size-8 { + font-size: 19px; +} +.tag-size-9 { + font-size: 20px; +} +.tag-size-10 { + font-size: 21px; +} +ul.tags, ul.tags.marked-tags, ul#related-tags { + list-style: none; + margin: 0; + padding: 0; + line-height: 170%; + display: block; +} +ul.tags li { + float: left; + display: block; + margin: 0 8px 0 0; + padding: 0; + height: 20px; +} +.wildcard-tags { + clear: both; +} +ul.tags.marked-tags li, .wildcard-tags ul.tags li { + margin-bottom: 5px; +} +#tagSelector div.inputs { + clear: both; + float: none; + margin-bottom: 10px; +} +.tags-page ul.tags li, ul#ab-user-tags li { + width: 160px; + margin: 5px; +} +ul#related-tags li { + margin: 0 5px 8px 0; + float: left; + clear: left; +} +/* .tag-left and .tag-right are for the sliding doors decoration of tags */ +.tag-left { + cursor: pointer; + display: block; + float: left; + height: 17px; + margin: 0 5px 0 0; + padding: 0; + -webkit-box-shadow: 0px 0px 5px #d3d6d7; + -moz-box-shadow: 0px 0px 5px #d3d6d7; + box-shadow: 0px 0px 5px #d3d6d7; +} +.tag-right { + background: #f3f6f6; + border: #fff 1px solid ; + border-top: #fff 2px solid; + outline: #cfdbdb 1px solid; + /* .box-shadow(0px,1px,0px,#88a8a8);*/ + + display: block; + float: left; + height: 17px; + line-height: 17px; + font-weight: normal; + font-size: 11px; + padding: 0px 8px 0px 8px; + text-decoration: none; + text-align: center; + white-space: nowrap; + vertical-align: middle; + font-family: Arial; + color: #717179; +} +.deletable-tag { + margin-right: 3px; + white-space: nowrap; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; + -webkit-border-bottom-right-radius: 4px; + -webkit-border-top-right-radius: 4px; +} +.tags a.tag-right, .tags span.tag-right { + color: #585858; + text-decoration: none; +} +.tags a:hover { + color: #1A1A1A; +} +.users-page h1, .tags-page h1 { + float: left; +} +.main-page h1 { + margin-right: 5px; +} +.delete-icon { + margin-top: -1px; + float: left; + height: 21px; + width: 18px; + display: block; + line-height: 20px; + text-align: center; + background: #bbcdcd; + cursor: default; + color: #fff; + border-top: #cfdbdb 1px solid; + font-family: Arial; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; + -webkit-border-bottom-right-radius: 4px; + -webkit-border-top-right-radius: 4px; + text-shadow: 0px 1px 0px #7ea0a0; + -moz-text-shadow: 0px 1px 0px #7ea0a0; + -webkit-text-shadow: 0px 1px 0px #7ea0a0; +} +.delete-icon:hover { + background: #b32f2f; +} +.tag-number { + font-weight: normal; + float: left; + font-size: 16px; + color: #5d5d5d; +} +.badges .tag-number { + float: none; + display: inline; + padding-right: 15px; +} +/* ----- Ask and Edit Question Form template----- */ +.section-title { + color: #7ea9b3; + font-family: 'Yanone Kaffeesatz', sans-serif; + font-weight: bold; + font-size: 24px; +} +#fmask { + margin-bottom: 30px; + width: 100%; +} +#askFormBar { + display: inline-block; + padding: 4px 7px 5px 0px; + margin-top: 0px; +} +#askFormBar p { + margin: 0 0 5px 0; + font-size: 14px; + color: #525252; + line-height: 1.4; +} +#askFormBar .questionTitleInput { + font-size: 24px; + line-height: 24px; + height: 36px; + margin: 0px; + padding: 0px 0 0 5px; + border: #cce6ec 3px solid; + width: 725px; +} +.ask-page div#question-list, .edit-question-page div#question-list { + float: none; + border-bottom: #f0f0ec 1px solid; + float: left; + margin-bottom: 10px; +} +.ask-page div#question-list a, .edit-question-page div#question-list a { + line-height: 30px; +} +.ask-page div#question-list h2, .edit-question-page div#question-list h2 { + font-size: 13px; + padding-bottom: 0; + color: #1b79bd; + border-top: #f0f0ec 1px solid; + border-left: #f0f0ec 1px solid; + height: 30px; + line-height: 30px; + font-weight: normal; +} +.ask-page div#question-list span, .edit-question-page div#question-list span { + width: 28px; + height: 26px; + line-height: 26px; + text-align: center; + margin-right: 10px; + float: left; + display: block; + color: #fff; + background: #b8d0d5; + border-radius: 3px; + -ms-border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + -khtml-border-radius: 3px; +} +.ask-page label, .edit-question-page label { + color: #525252; + font-size: 13px; +} +.ask-page #id_tags, .edit-question-page #id_tags { + border: #cce6ec 3px solid; + height: 25px; + padding-left: 5px; + width: 395px; + font-size: 14px; +} +.title-desc { + color: #707070; + font-size: 13px; +} +#fmanswer input.submit, .ask-page input.submit, .edit-question-page input.submit { + float: left; + background: url(../images/medium-button.png) top repeat-x; + height: 34px; + border: 0; + font-family: 'Yanone Kaffeesatz', sans-serif; + color: #4a757f; + font-weight: normal; + font-size: 21px; + margin-top: 3px; + border-radius: 4px; + -ms-border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -khtml-border-radius: 4px; + -webkit-box-shadow: 1px 1px 2px #636363; + -moz-box-shadow: 1px 1px 2px #636363; + box-shadow: 1px 1px 2px #636363; + margin-right: 7px; +} +#fmanswer input.submit:hover, .ask-page input.submit:hover, .edit-question-page input.submit:hover { + text-decoration: none; + background: url(../images/medium-button.png) bottom repeat-x; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; +} +#editor { + /*adjustment for editor preview*/ + + font-size: 100%; + min-height: 200px; + line-height: 18px; + margin: 0; + border-left: #cce6ec 3px solid; + border-bottom: #cce6ec 3px solid; + border-right: #cce6ec 3px solid; + border-top: 0; + padding: 10px; + margin-bottom: 10px; + width: 717px; +} +#id_title { + width: 100%; +} +.wmd-preview { + margin: 3px 0 5px 0; + padding: 6px; + background-color: #F5F5F5; + min-height: 20px; + overflow: auto; + font-size: 13px; + font-family: Arial; +} +.wmd-preview p { + margin-bottom: 14px; + line-height: 1.4; + font-size: 14px; +} +.wmd-preview pre { + background-color: #E7F1F8; +} +.wmd-preview blockquote { + background-color: #eee; +} +.wmd-preview IMG { + max-width: 600px; +} +.preview-toggle { + width: 100%; + color: #b6a475; + /*letter-spacing:1px;*/ + + text-align: left; +} +.preview-toggle span:hover { + cursor: pointer; +} +.after-editor { + margin-top: 15px; + margin-bottom: 15px; +} +.checkbox { + margin-left: 5px; + font-weight: normal; + cursor: help; +} +.question-options { + margin-top: 1px; + color: #666; + line-height: 13px; + margin-bottom: 5px; +} +.question-options label { + vertical-align: text-bottom; +} +.edit-content-html { + border-top: 1px dotted #D8D2A9; + border-bottom: 1px dotted #D8D2A9; + margin: 5px 0 5px 0; +} +.edit-question-page, #fmedit, .wmd-preview { + color: #525252; +} +.edit-question-page #id_revision, #fmedit #id_revision, .wmd-preview #id_revision { + font-size: 14px; + margin-top: 5px; + margin-bottom: 5px; +} +.edit-question-page #id_title, #fmedit #id_title, .wmd-preview #id_title { + font-size: 24px; + line-height: 24px; + height: 36px; + margin: 0px; + padding: 0px 0 0 5px; + border: #cce6ec 3px solid; + width: 725px; + margin-bottom: 10px; +} +.edit-question-page #id_summary, #fmedit #id_summary, .wmd-preview #id_summary { + border: #cce6ec 3px solid; + height: 25px; + padding-left: 5px; + width: 395px; + font-size: 14px; +} +.edit-question-page .title-desc, #fmedit .title-desc, .wmd-preview .title-desc { + margin-bottom: 10px; +} +/* ----- Question template ----- */ +.question-page h1 { + padding-top: 0px; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +.question-page h1 a { + color: #464646; + font-size: 30px; + font-weight: normal; + line-height: 1; +} +.question-page p.rss { + float: none; + clear: both; + padding: 3px 0 0 23px; + font-size: 15px; + width: 110px; + background-position: center left; + margin-left: 0px !important; +} +.question-page p.rss a { + font-family: 'Yanone Kaffeesatz', sans-serif; + vertical-align: top; +} +.question-page .question-content { + float: right; + width: 682px; + margin-bottom: 10px; +} +.question-page #question-table { + float: left; + border-top: #f0f0f0 1px solid; +} +.question-page #question-table, .question-page .answer-table { + margin: 6px 0 6px 0; + border-spacing: 0px; + width: 670px; + padding-right: 10px; +} +.question-page .answer-table { + margin-top: 0px; + border-bottom: 1px solid #D4D4D4; + float: right; +} +.question-page .answer-table td, .question-page #question-table td { + width: 20px; + vertical-align: top; +} +.question-page .question-body, .question-page .answer-body { + overflow: auto; + margin-top: 10px; + font-family: Arial; + color: #4b4b4b; +} +.question-page .question-body p, .question-page .answer-body p { + margin-bottom: 14px; + line-height: 1.4; + font-size: 14px; + padding: 0px 5px 5px 0px; +} +.question-page .question-body a, .question-page .answer-body a { + color: #1b79bd; +} +.question-page .question-body li, .question-page .answer-body li { + margin-bottom: 7px; +} +.question-page .question-body IMG, .question-page .answer-body IMG { + max-width: 600px; +} +.question-page .post-update-info-container { + float: right; + width: 175px; +} +.question-page .post-update-info { + background: #ffffff url(../images/background-user-info.png) repeat-x bottom; + float: right; + font-size: 9px; + font-family: Arial; + width: 158px; + padding: 4px; + margin: 0px 0px 5px 5px; + line-height: 14px; + border-radius: 4px; + -ms-border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -khtml-border-radius: 4px; + -webkit-box-shadow: 0px 2px 1px #bfbfbf; + -moz-box-shadow: 0px 2px 1px #bfbfbf; + box-shadow: 0px 2px 1px #bfbfbf; +} +.question-page .post-update-info p { + line-height: 13px; + font-size: 11px; + margin: 0 0 2px 1px; + padding: 0; +} +.question-page .post-update-info a { + color: #444; +} +.question-page .post-update-info .gravatar { + float: left; + margin-right: 4px; +} +.question-page .post-update-info p.tip { + color: #444; + line-height: 13px; + font-size: 10px; +} +.question-page .post-controls { + font-size: 11px; + line-height: 12px; + min-width: 200px; + padding-left: 5px; + text-align: right; + clear: left; + float: right; + margin-top: 10px; + margin-bottom: 8px; +} +.question-page .post-controls a { + color: #777; + padding: 0px 3px 3px 22px; + cursor: pointer; + border: none; + font-size: 12px; + font-family: Arial; + text-decoration: none; + height: 18px; + display: block; + float: right; + line-height: 18px; + margin-top: -2px; + margin-left: 4px; +} +.question-page .post-controls a:hover { + background-color: #f5f0c9; + border-radius: 3px; + -ms-border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + -khtml-border-radius: 3px; +} +.question-page .post-controls .sep { + color: #ccc; + float: right; + height: 18px; + font-size: 18px; +} +.question-page .post-controls .question-delete, .question-page .answer-controls .question-delete { + background: url(../images/delete.png) no-repeat center left; + padding-left: 16px; +} +.question-page .post-controls .question-flag, .question-page .answer-controls .question-flag { + background: url(../images/flag.png) no-repeat center left; +} +.question-page .post-controls .question-edit, .question-page .answer-controls .question-edit { + background: url(../images/edit2.png) no-repeat center left; +} +.question-page .post-controls .question-retag, .question-page .answer-controls .question-retag { + background: url(../images/retag.png) no-repeat center left; +} +.question-page .post-controls .question-close, .question-page .answer-controls .question-close { + background: url(../images/close.png) no-repeat center left; +} +.question-page .post-controls .permant-link, .question-page .answer-controls .permant-link { + background: url(../images/link.png) no-repeat center left; +} +.question-page .tabBar { + width: 100%; +} +.question-page #questionCount { + float: left; + font-family: 'Yanone Kaffeesatz', sans-serif; + line-height: 15px; +} +.question-page .question-img-upvote, +.question-page .question-img-downvote, +.question-page .answer-img-upvote, +.question-page .answer-img-downvote { + width: 25px; + height: 20px; + cursor: pointer; +} +.question-page .question-img-upvote, .question-page .answer-img-upvote { + background: url(../images/vote-arrow-up-new.png) no-repeat; +} +.question-page .question-img-downvote, .question-page .answer-img-downvote { + background: url(../images/vote-arrow-down-new.png) no-repeat; +} +.question-page .question-img-upvote:hover, +.question-page .question-img-upvote.on, +.question-page .answer-img-upvote:hover, +.question-page .answer-img-upvote.on { + background: url(../images/vote-arrow-up-on-new.png) no-repeat; +} +.question-page .question-img-downvote:hover, +.question-page .question-img-downvote.on, +.question-page .answer-img-downvote:hover, +.question-page .answer-img-downvote.on { + background: url(../images/vote-arrow-down-on-new.png) no-repeat; +} +.question-page #fmanswer_button { + margin: 8px 0px ; +} +.question-page .question-img-favorite:hover { + background: url(../images/vote-favorite-on.png); +} +.question-page div.comments { + padding: 0; +} +.question-page #comment-title { + font-weight: bold; + font-size: 23px; + color: #7ea9b3; + width: 200px; + float: left; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +.question-page .comments { + font-size: 12px; + clear: both; + /* A small hack to solve 1px problem on webkit browsers */ + +} +.question-page .comments div.controls { + clear: both; + float: left; + width: 100%; + margin: 3px 0 20px 5px; +} +.question-page .comments .controls a { + color: #988e4c; + padding: 0 3px 2px 22px; + font-family: Arial; + font-size: 13px; + background: url(../images/comment.png) no-repeat center left; +} +.question-page .comments .controls a:hover { + background-color: #f5f0c9; + text-decoration: none; +} +.question-page .comments .button { + color: #988e4c; + font-size: 11px; + padding: 3px; + cursor: pointer; +} +.question-page .comments a { + background-color: inherit; + color: #1b79bd; + padding: 0; +} +.question-page .comments form.post-comments { + margin: 3px 26px 0 42px; +} +.question-page .comments form.post-comments textarea { + font-size: 13px; + line-height: 1.3; +} +.question-page .comments textarea { + height: 42px; + width: 100%; + margin: 7px 0 5px 1px; + font-family: Arial; + outline: none; + overflow: auto; + font-size: 12px; + line-height: 140%; + padding-left: 2px; + padding-top: 3px; + border: #cce6ec 3px solid; +} +@media screen and (-webkit-min-device-pixel-ratio:0) { + textarea { + padding-left: 3px !important; + } +} +.question-page .comments input { + margin-left: 10px; + margin-top: 1px; + vertical-align: top; + width: 100px; +} +.question-page .comments button { + background: url(../images/small-button-blue.png) repeat-x top; + border: 0; + color: #4a757f; + font-family: Arial; + font-size: 13px; + width: 100px; + font-weight: bold; + height: 27px; + line-height: 25px; + margin-bottom: 5px; + cursor: pointer; + border-radius: 4px; + -ms-border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -khtml-border-radius: 4px; + text-shadow: 0px 1px 0px #e6f6fa; + -moz-text-shadow: 0px 1px 0px #e6f6fa; + -webkit-text-shadow: 0px 1px 0px #e6f6fa; + -webkit-box-shadow: 1px 1px 2px #808080; + -moz-box-shadow: 1px 1px 2px #808080; + box-shadow: 1px 1px 2px #808080; +} +.question-page .comments button:hover { + background: url(../images/small-button-blue.png) bottom repeat-x; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; +} +.question-page .comments .counter { + display: inline-block; + width: 245px; + float: right; + color: #b6a475 !important; + vertical-align: top; + font-family: Arial; + float: right; + text-align: right; +} +.question-page .comments .comment { + border-bottom: 1px solid #edeeeb; + clear: both; + margin: 0; + margin-top: 8px; + padding-bottom: 4px; + overflow: auto; + font-family: Arial; + font-size: 11px; + min-height: 25px; + background: #ffffff url(../images/comment-background.png) bottom repeat-x; + border-radius: 5px; + -ms-border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + -khtml-border-radius: 5px; +} +.question-page .comments div.comment:hover { + background-color: #efefef; +} +.question-page .comments a.author { + background-color: inherit; + color: #1b79bd; + padding: 0; +} +.question-page .comments a.author:hover { + text-decoration: underline; +} +.question-page .comments span.delete-icon { + background: url(../images/close-small.png) no-repeat; + border: 0; + width: 14px; + height: 14px; +} +.question-page .comments span.delete-icon:hover { + border: #BC564B 2px solid; + border-radius: 10px; + -ms-border-radius: 10px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + -khtml-border-radius: 10px; + margin: -3px 0px 0px -2px; +} +.question-page .comments .content { + margin-bottom: 7px; +} +.question-page .comments .comment-votes { + float: left; + width: 37px; + line-height: 130%; + padding: 6px 5px 6px 3px; +} +.question-page .comments .comment-body { + line-height: 1.3; + margin: 3px 26px 0 46px; + padding: 5px 3px; + color: #666; + font-size: 13px; +} +.question-page .comments .comment-body .edit { + padding-left: 6px; +} +.question-page .comments .comment-body p { + font-size: 13px; + line-height: 1.3; + margin-bottom: 3px; + padding: 0; +} +.question-page .comments .comment-delete { + float: right; + width: 14px; + line-height: 130%; + padding: 8px 6px; +} +.question-page .comments .upvote { + margin: 0px; + padding-right: 17px; + padding-top: 2px; + text-align: right; + height: 20px; + font-size: 13px; + font-weight: bold; + color: #777; +} +.question-page .comments .upvote.upvoted { + color: #d64000; +} +.question-page .comments .upvote.hover { + background: url(../images/go-up-grey.png) no-repeat; + background-position: right 1px; +} +.question-page .comments .upvote:hover { + background: url(../images/go-up-orange.png) no-repeat; + background-position: right 1px; +} +.question-page .comments .help-text { + float: right; + text-align: right; + color: gray; + margin-bottom: 0px; + margin-top: 0px; + line-height: 50%; +} +.question-page #questionTools { + font-size: 22px; + margin-top: 11px; + text-align: left; +} +.question-page .question-status { + margin-top: 10px; + margin-bottom: 15px; + padding: 20px; + background-color: #fef7cc; + text-align: center; + border: #e1c04a 1px solid; +} +.question-page .question-status h3 { + font-size: 20px; + color: #707070; + font-weight: normal; +} +.question-page .vote-buttons { + float: left; + text-align: center; + padding-top: 2px; + margin: 10px 10px 0px 3px; +} +.question-page .vote-buttons IMG { + cursor: pointer; +} +.question-page .vote-number { + font-family: 'Yanone Kaffeesatz', sans-serif; + padding: 0px 0 5px 0; + font-size: 25px; + font-weight: bold; + color: #777; +} +.question-page .vote-buttons .notify-sidebar { + text-align: left; + width: 120px; +} +.question-page .vote-buttons .notify-sidebar label { + vertical-align: top; +} +.question-page .tabBar-answer { + margin-bottom: 15px; + padding-left: 7px; + width: 723px; + margin-top: 10px; +} +.question-page .answer .vote-buttons { + float: left; +} +.question-page .accepted-answer { + background-color: #f7fecc; + border-bottom-color: #9BD59B; +} +.question-page .accepted-answer .vote-buttons { + width: 27px; + margin-right: 10px; + margin-top: 10px; +} +.question-page .answer .post-update-info a { + color: #444444; +} +.question-page .answered { + background: #CCC; + color: #999; +} +.question-page .answered-accepted { + background: #DCDCDC; + color: #763333; +} +.question-page .answered-accepted strong { + color: #E1E818; +} +.question-page .answered-by-owner { + background: #F1F1FF; +} +.question-page .answered-by-owner .comments .button { + background-color: #E6ECFF; +} +.question-page .answered-by-owner .comments { + background-color: #E6ECFF; +} +.question-page .answered-by-owner .vote-buttons { + margin-right: 10px; +} +.question-page .answer-img-accept:hover { + background: url(../images/vote-accepted-on.png); +} +.question-page .answer-body a { + color: #1b79bd; +} +.question-page .answer-body li { + margin-bottom: 0.7em; +} +.question-page #fmanswer { + color: #707070; + line-height: 1.2; + margin-top: 10px; +} +.question-page #fmanswer h2 { + font-family: 'Yanone Kaffeesatz', sans-serif; + color: #7ea9b3; + font-size: 24px; +} +.question-page #fmanswer label { + font-size: 13px; +} +.question-page .message { + padding: 5px; + margin: 0px 0 10px 0; +} +.facebook-share.icon, +.twitter-share.icon, +.linkedin-share.icon, +.identica-share.icon { + background: url(../images/socialsprite.png) no-repeat; + display: block; + text-indent: -100em; + height: 25px; + width: 25px; + margin-bottom: 3px; +} +.facebook-share.icon:hover, +.twitter-share.icon:hover, +.linkedin-share.icon:hover, +.identica-share.icon:hover { + opacity: 0.8; + filter: alpha(opacity=80); +} +.facebook-share.icon { + background-position: -26px 0px; +} +.identica-share.icon { + background-position: -78px 0px; +} +.twitter-share.icon { + margin-top: 10px; + background-position: 0px 0px; +} +.linkedin-share.icon { + background-position: -52px 0px; +} +/* -----Content pages, Login, About, FAQ, Users----- */ +.openid-signin, +.meta, +.users-page, +.user-profile-edit-page { + font-size: 13px; + line-height: 1.3; + color: #525252; +} +.openid-signin p, +.meta p, +.users-page p, +.user-profile-edit-page p { + font-size: 13px; + color: #707070; + line-height: 1.3; + font-family: Arial; + color: #525252; + margin-bottom: 12px; +} +.openid-signin h2, +.meta h2, +.users-page h2, +.user-profile-edit-page h2 { + color: #525252; + padding-left: 0px; + font-size: 16px; +} +.openid-signin form, +.meta form, +.users-page form, +.user-profile-edit-page form, +.user-profile-page form { + margin-bottom: 15px; +} +.openid-signin input[type="text"], +.meta input[type="text"], +.users-page input[type="text"], +.user-profile-edit-page input[type="text"], +.user-profile-page input[type="text"], +.openid-signin input[type="password"], +.meta input[type="password"], +.users-page input[type="password"], +.user-profile-edit-page input[type="password"], +.user-profile-page input[type="password"], +.openid-signin select, +.meta select, +.users-page select, +.user-profile-edit-page select, +.user-profile-page select { + border: #cce6ec 3px solid; + height: 25px; + padding-left: 5px; + width: 395px; + font-size: 14px; +} +.openid-signin select, +.meta select, +.users-page select, +.user-profile-edit-page select, +.user-profile-page select { + width: 405px; + height: 30px; +} +.openid-signin textarea, +.meta textarea, +.users-page textarea, +.user-profile-edit-page textarea, +.user-profile-page textarea { + border: #cce6ec 3px solid; + padding-left: 5px; + padding-top: 5px; + width: 395px; + font-size: 14px; +} +.openid-signin input.submit, +.meta input.submit, +.users-page input.submit, +.user-profile-edit-page input.submit, +.user-profile-page input.submit { + background: url(../images/small-button-blue.png) repeat-x top; + border: 0; + color: #4a757f; + font-weight: bold; + font-size: 13px; + font-family: Arial; + height: 26px; + margin: 5px 0px; + width: 100px; + cursor: pointer; + border-radius: 4px; + -ms-border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -khtml-border-radius: 4px; + text-shadow: 0px 1px 0px #e6f6fa; + -moz-text-shadow: 0px 1px 0px #e6f6fa; + -webkit-text-shadow: 0px 1px 0px #e6f6fa; + -webkit-box-shadow: 1px 1px 2px #808080; + -moz-box-shadow: 1px 1px 2px #808080; + box-shadow: 1px 1px 2px #808080; +} +.openid-signin input.submit:hover, +.meta input.submit:hover, +.users-page input.submit:hover, +.user-profile-edit-page input.submit:hover, +.user-profile-page input.submit:hover { + background: url(../images/small-button-blue.png) repeat-x bottom; + text-decoration: none; +} +.openid-signin .cancel, +.meta .cancel, +.users-page .cancel, +.user-profile-edit-page .cancel, +.user-profile-page .cancel { + background: url(../images/small-button-cancel.png) repeat-x top !important; + color: #525252 !important; +} +.openid-signin .cancel:hover, +.meta .cancel:hover, +.users-page .cancel:hover, +.user-profile-edit-page .cancel:hover, +.user-profile-page .cancel:hover { + background: url(../images/small-button-cancel.png) repeat-x bottom !important; +} +#email-input-fs, +#local_login_buttons, +#password-fs, +#openid-fs { + margin-top: 10px; +} +#email-input-fs #id_email, +#local_login_buttons #id_email, +#password-fs #id_email, +#openid-fs #id_email, +#email-input-fs #id_username, +#local_login_buttons #id_username, +#password-fs #id_username, +#openid-fs #id_username, +#email-input-fs #id_password, +#local_login_buttons #id_password, +#password-fs #id_password, +#openid-fs #id_password { + font-size: 12px; + line-height: 20px; + height: 20px; + margin: 0px; + padding: 0px 0 0 5px; + border: #cce6ec 3px solid; + width: 200px; +} +#email-input-fs .submit-b, +#local_login_buttons .submit-b, +#password-fs .submit-b, +#openid-fs .submit-b { + background: url(../images/small-button-blue.png) repeat-x top; + border: 0; + color: #4a757f; + font-weight: bold; + font-size: 13px; + font-family: Arial; + height: 24px; + margin-top: -2px; + padding-left: 10px; + padding-right: 10px; + cursor: pointer; + border-radius: 4px; + -ms-border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -khtml-border-radius: 4px; + text-shadow: 0px 1px 0px #e6f6fa; + -moz-text-shadow: 0px 1px 0px #e6f6fa; + -webkit-text-shadow: 0px 1px 0px #e6f6fa; + -webkit-box-shadow: 1px 1px 2px #808080; + -moz-box-shadow: 1px 1px 2px #808080; + box-shadow: 1px 1px 2px #808080; +} +#email-input-fs .submit-b:hover, +#local_login_buttons .submit-b:hover, +#password-fs .submit-b:hover, +#openid-fs .submit-b:hover { + background: url(../images/small-button-blue.png) repeat-x bottom; +} +.openid-input { + background: url(../images/openid.gif) no-repeat; + padding-left: 15px; + cursor: pointer; +} +.openid-login-input { + background-position: center left; + background: url(../images/openid.gif) no-repeat 0% 50%; + padding: 5px 5px 5px 15px; + cursor: pointer; + font-family: Trebuchet MS; + font-weight: 300; + font-size: 150%; + width: 500px; +} +.openid-login-submit { + height: 40px; + width: 80px; + line-height: 40px; + cursor: pointer; + border: 1px solid #777; + font-weight: bold; + font-size: 120%; +} +/* People page */ +.tabBar-user { + width: 375px; +} +.user { + padding: 5px; + line-height: 140%; + width: 166px; + border: #eee 1px solid; + margin-bottom: 5px; + border-radius: 3px; + -ms-border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + -khtml-border-radius: 3px; +} +.user .user-micro-info { + color: #525252; +} +.user ul { + margin: 0; + list-style-type: none; +} +.user .thumb { + clear: both; + float: left; + margin-right: 4px; + display: inline; +} +/* tags page */ +.tabBar-tags { + width: 270px; + margin-bottom: 15px; +} +/* badges page */ +a.medal { + font-size: 17px; + line-height: 250%; + margin-right: 5px; + color: #333; + text-decoration: none; + background: url(../images/medala.gif) no-repeat; + border-left: 1px solid #EEE; + border-top: 1px solid #EEE; + border-bottom: 1px solid #CCC; + border-right: 1px solid #CCC; + padding: 4px 12px 4px 6px; +} +a:hover.medal { + color: #333; + text-decoration: none; + background: url(../images/medala_on.gif) no-repeat; + border-left: 1px solid #E7E296; + border-top: 1px solid #E7E296; + border-bottom: 1px solid #D1CA3D; + border-right: 1px solid #D1CA3D; +} +#award-list .user { + float: left; + margin: 5px; +} +/* profile page */ +.tabBar-profile { + width: 100%; + margin-bottom: 15px; + float: left; +} +.user-profile-page { + font-size: 13px; + color: #525252; +} +.user-profile-page p { + font-size: 13px; + line-height: 1.3; + color: #525252; +} +.user-profile-page .avatar img { + border: #eee 1px solid; + padding: 5px; +} +.user-profile-page h2 { + padding: 10px 0px 10px 0px; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +.user-details { + font-size: 13px; +} +.user-details h3 { + font-size: 16px; +} +.user-about { + background-color: #EEEEEE; + height: 200px; + line-height: 20px; + overflow: auto; + padding: 10px; + width: 90%; +} +.user-about p { + font-size: 13px; +} +.follow-toggle, .submit { + border: 0 !important; + color: #4a757f; + font-weight: bold; + font-size: 12px; + height: 26px; + line-height: 26px; + margin-top: -2px; + font-size: 15px; + cursor: pointer; + font-family: 'Yanone Kaffeesatz', sans-serif; + background: url(../images/small-button-blue.png) repeat-x top; + border-radius: 4px; + -ms-border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -khtml-border-radius: 4px; + text-shadow: 0px 1px 0px #e6f6fa; + -moz-text-shadow: 0px 1px 0px #e6f6fa; + -webkit-text-shadow: 0px 1px 0px #e6f6fa; + -webkit-box-shadow: 1px 1px 2px #808080; + -moz-box-shadow: 1px 1px 2px #808080; + box-shadow: 1px 1px 2px #808080; +} +.follow-toggle:hover, .submit:hover { + background: url(../images/small-button-blue.png) repeat-x bottom; + text-decoration: none !important; +} +.follow-toggle .follow { + font-color: #000; + font-style: normal; +} +.follow-toggle .unfollow div.unfollow-red { + display: none; +} +.follow-toggle .unfollow:hover div.unfollow-red { + display: inline; + color: #fff; + font-weight: bold; + color: #A05736; +} +.follow-toggle .unfollow:hover div.unfollow-green { + display: none; +} +.count { + font-family: 'Yanone Kaffeesatz', sans-serif; + font-size: 200%; + font-weight: 700; + color: #777777; +} +.scoreNumber { + font-family: 'Yanone Kaffeesatz', sans-serif; + font-size: 35px; + font-weight: 800; + color: #777; + line-height: 40px; + /*letter-spacing:0px*/ + + margin-top: 3px; +} +.vote-count { + font-family: Arial; + font-size: 160%; + font-weight: 700; + color: #777; +} +.answer-summary { + display: block; + clear: both; + padding: 3px; +} +.answer-votes { + background-color: #EEEEEE; + color: #555555; + float: left; + font-family: Arial; + font-size: 15px; + font-weight: bold; + height: 17px; + padding: 2px 4px 5px; + text-align: center; + text-decoration: none; + width: 20px; + margin-right: 10px; + border-radius: 4px; + -ms-border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -khtml-border-radius: 4px; +} +.karma-summary { + padding: 5px; + font-size: 13px; +} +.karma-summary h3 { + text-align: center; + font-weight: bold; + padding: 5px; +} +.karma-diagram { + width: 477px; + height: 300px; + float: left; + margin-right: 10px; +} +.karma-details { + float: right; + width: 450px; + height: 250px; + overflow-y: auto; + word-wrap: break-word; +} +.karma-details p { + margin-bottom: 10px; +} +.karma-gained { + font-weight: bold; + background: #eee; + width: 25px; + margin-right: 5px; + color: green; + padding: 3px; + display: block; + float: left; + text-align: center; + border-radius: 3px; + -ms-border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + -khtml-border-radius: 3px; +} +.karma-lost { + font-weight: bold; + background: #eee; + width: 25px; + color: red; + padding: 3px; + display: block; + margin-right: 5px; + float: left; + text-align: center; + border-radius: 3px; + -ms-border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + -khtml-border-radius: 3px; +} +.submit-row { + margin-bottom: 10px; +} +/*----- Revision pages ----- */ +.revision { + margin: 10px 0 10px 0; + font-size: 13px; + color: #525252; +} +.revision p { + font-size: 13px; + line-height: 1.3; + color: #525252; +} +.revision h3 { + font-family: 'Yanone Kaffeesatz', sans-serif; + font-size: 21px; + padding-left: 0px; +} +.revision .header { + background-color: #F5F5F5; + padding: 5px; + cursor: pointer; +} +.revision .author { + background-color: #e9f3f5; +} +.revision .summary { + padding: 5px 0 10px 0; +} +.revision .summary span { + background-color: #fde785; + padding: 6px; + border-radius: 4px; + -ms-border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -khtml-border-radius: 4px; + display: inline; + -webkit-box-shadow: 1px 1px 4px #cfb852; + -moz-box-shadow: 1px 1px 4px #cfb852; + box-shadow: 1px 1px 4px #cfb852; +} +.revision .answerbody { + padding: 10px 0 5px 10px; +} +.revision .revision-mark { + width: 150px; + text-align: left; + display: inline-block; + font-size: 11px; + overflow: hidden; +} +.revision .revision-mark .gravatar { + float: left; + margin-right: 4px; + padding-top: 5px; +} +.revision .revision-number { + font-size: 300%; + font-weight: bold; + font-family: sans-serif; +} +del, del .post-tag { + color: #C34719; +} +ins .post-tag, ins p, ins { + background-color: #E6F0A2; +} +/* ----- Red Popup notification ----- */ +.vote-notification { + z-index: 1; + cursor: pointer; + display: none; + position: absolute; + font-family: Arial; + font-size: 14px; + font-weight: normal; + color: white; + background-color: #8e0000; + text-align: center; + padding-bottom: 10px; + -webkit-box-shadow: 0px 2px 4px #370000; + -moz-box-shadow: 0px 2px 4px #370000; + box-shadow: 0px 2px 4px #370000; + border-radius: 4px; + -ms-border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -khtml-border-radius: 4px; +} +.vote-notification h3 { + background: url(../images/notification.png) repeat-x top; + padding: 10px 10px 10px 10px; + font-size: 13px; + margin-bottom: 5px; + border-top: #8e0000 1px solid; + color: #fff; + font-weight: normal; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; + -webkit-border-top-right-radius: 4px; +} +.vote-notification a { + color: #fb7321; + text-decoration: underline; + font-weight: bold; +} +/* ----- Footer links , check blocks/footer.html----- */ +#ground { + width: 100%; + clear: both; + border-top: 1px solid #000; + padding: 6px 0 0 0; + background: #16160f; + font-size: 16px; + font-family: 'Yanone Kaffeesatz', sans-serif; +} +#ground p { + margin-bottom: 0; +} +.footer-links { + color: #EEE; + text-align: left; + width: 500px; + float: left; +} +.footer-links a { + color: #e7e8a8; +} +.powered-link { + width: 500px; + float: left; + text-align: left; +} +.powered-link a { + color: #8ebcc7; +} +.copyright { + color: #616161; + width: 450px; + float: right; + text-align: right; +} +.copyright a { + color: #8ebcc7; +} +.copyright img.license-logo { + margin: 6px 0px 20px 10px; + float: right; +} +.notify-me { + float: left; +} +span.text-counter { + margin-right: 20px; +} +span.form-error { + color: #990000; + font-weight: normal; + margin-left: 5px; +} +ul.errorlist { + margin-bottom: 0; +} +p.form-item { + margin: 0px; +} +.deleted { + background: #F4E7E7 none repeat scroll 0 0; +} +/* openid styles */ +.form-row { + line-height: 25px; +} +table.form-as-table { + margin-top: 5px; +} +table.form-as-table ul { + list-style-type: none; + display: inline; +} +table.form-as-table li { + display: inline; +} +table.form-as-table td { + text-align: right; +} +table.form-as-table th { + text-align: left; + font-weight: normal; +} +table.ab-subscr-form { + width: 45em; +} +table.ab-tag-filter-form { + width: 45em; +} +.submit-row { + line-height: 30px; + padding-top: 10px; + display: block; + clear: both; +} +.errors { + line-height: 20px; + color: red; +} +.error { + color: darkred; + margin: 0; + font-size: 10px; +} +label.retag-error { + color: darkred; + padding-left: 5px; + font-size: 10px; +} +.fieldset { + border: none; + margin-top: 10px; + padding: 10px; +} +span.form-error { + color: #990000; + font-size: 90%; + font-weight: normal; + margin-left: 5px; +} +/* +.favorites-count-off { + color: #919191; + float: left; + text-align: center; +} + +.favorites-count { + color: #D4A849; + float: left; + text-align: center; +} +*/ +/* todo: get rid of this in html */ +.favorites-empty { + width: 32px; + height: 45px; + float: left; +} +.user-info-table { + margin-bottom: 10px; + border-spacing: 0; +} +/* todo: remove this hack? */ +.user-stats-table .narrow { + width: 660px; +} +.narrow .summary h3 { + padding: 0px; + margin: 0px; +} +.relativetime { + font-weight: bold; + text-decoration: none; +} +.narrow .tags { + float: left; +} +/* todo: make these more semantic */ +.user-action-1 { + font-weight: bold; + color: #333; +} +.user-action-2 { + font-weight: bold; + color: #CCC; +} +.user-action-3 { + color: #333; +} +.user-action-4 { + color: #333; +} +.user-action-5 { + color: darkred; +} +.user-action-6 { + color: darkred; +} +.user-action-7 { + color: #333; +} +.user-action-8 { + padding: 3px; + font-weight: bold; + background-color: #CCC; + color: #763333; +} +.revision-summary { + background-color: #FFFE9B; + padding: 2px; +} +.question-title-link a { + font-weight: bold; + color: #0077CC; +} +.answer-title-link a { + color: #333; +} +/* todo: make these more semantic */ +.post-type-1 a { + font-weight: bold; +} +.post-type-3 a { + font-weight: bold; +} +.post-type-5 a { + font-weight: bold; +} +.post-type-2 a { + color: #333; +} +.post-type-4 a { + color: #333; +} +.post-type-6 a { + color: #333; +} +.post-type-8 a { + color: #333; +} +.hilite { + background-color: #ff0; +} +.hilite1 { + background-color: #ff0; +} +.hilite2 { + background-color: #f0f; +} +.hilite3 { + background-color: #0ff; +} +.gold, .badge1 { + color: #FFCC00; +} +.silver, .badge2 { + color: #CCCCCC; +} +.bronze, .badge3 { + color: #CC9933; +} +.score { + font-weight: 800; + color: #333; +} +a.comment { + background: #EEE; + color: #993300; + padding: 5px; +} +a.offensive { + color: #999; +} +.message h1 { + padding-top: 0px; + font-size: 15px; +} +.message p { + margin-bottom: 0px; +} +p.space-above { + margin-top: 10px; +} +.warning { + color: red; +} +button::-moz-focus-inner { + padding: 0; + border: none; +} +.submit { + cursor: pointer; + /*letter-spacing:1px;*/ + + background-color: #D4D0C8; + height: 30px; + border: 1px solid #777777; + /* width:100px; */ + + font-weight: bold; + font-size: 120%; +} +.submit:hover { + text-decoration: underline; +} +.submit.small { + margin-right: 5px; + height: 20px; + font-weight: normal; + font-size: 12px; + padding: 1px 5px; +} +.submit.small:hover { + text-decoration: none; +} +.question-page a.submit { + display: -moz-inline-stack; + display: inline-block; + line-height: 30px; + padding: 0 5px; + *display: inline; +} +.noscript { + position: fixed; + top: 0px; + left: 0px; + width: 100%; + z-index: 100; + padding: 5px 0; + text-align: center; + font-family: sans-serif; + font-size: 120%; + font-weight: Bold; + color: #FFFFFF; + background-color: #AE0000; +} +.big { + font-size: 14px; +} +.strong { + font-weight: bold; +} +.orange { + /* used in django.po */ + + color: #d64000; + font-weight: bold; +} +.grey { + color: #808080; +} +.about div { + padding: 10px 5px 10px 5px; + border-top: 1px dashed #aaaaaa; +} +.highlight { + background-color: #FFF8C6; +} +.nomargin { + margin: 0; +} +.margin-bottom { + margin-bottom: 10px; +} +.margin-top { + margin-top: 10px; +} +.inline-block { + display: inline-block; +} +.action-status { + margin: 0; + border: none; + text-align: center; + line-height: 10px; + font-size: 12px; + padding: 0; +} +.action-status span { + padding: 3px 5px 3px 5px; + background-color: #fff380; + /* nice yellow */ + + font-weight: normal; + -moz-border-radius: 5px; + -khtml-border-radius: 5px; + -webkit-border-radius: 5px; +} +.list-table td { + vertical-align: top; +} +/* these need to go */ +table.form-as-table .errorlist { + display: block; + margin: 0; + padding: 0 0 0 5px; + text-align: left; + font-size: 10px; + color: darkred; +} +table.form-as-table input { + display: inline; + margin-left: 4px; +} +table.form-as-table th { + vertical-align: bottom; + padding-bottom: 4px; +} +.form-row-vertical { + margin-top: 8px; + display: block; +} +.form-row-vertical label { + margin-bottom: 3px; + display: block; +} +/* above stuff needs to go */ +.text-align-right { + text-align: center; +} +ul.form-horizontal-rows { + list-style: none; + margin: 0; +} +ul.form-horizontal-rows li { + position: relative; + height: 40px; +} +ul.form-horizontal-rows label { + display: inline-block; +} +ul.form-horizontal-rows ul.errorlist { + list-style: none; + color: darkred; + font-size: 10px; + line-height: 10px; + position: absolute; + top: 2px; + left: 180px; + text-align: left; + margin: 0; +} +ul.form-horizontal-rows ul.errorlist li { + height: 10px; +} +ul.form-horizontal-rows label { + position: absolute; + left: 0px; + bottom: 6px; + margin: 0px; + line-height: 12px; + font-size: 12px; +} +ul.form-horizontal-rows li input { + position: absolute; + bottom: 0px; + left: 180px; + margin: 0px; +} +.narrow .summary { + float: left; +} +.user-profile-tool-links { + font-weight: bold; + vertical-align: top; +} +ul.post-tags { + margin-left: 3px; +} +ul.post-tags li { + margin-top: 4px; + margin-bottom: 3px; +} +ul.post-retag { + margin-bottom: 0px; + margin-left: 5px; +} +#question-controls .tags { + margin: 0 0 3px 0; +} +#tagSelector { + padding-bottom: 2px; + margin-bottom: 0; +} +#related-tags { + padding-left: 3px; +} +#hideIgnoredTagsControl { + margin: 5px 0 0 0; +} +#hideIgnoredTagsControl label { + font-size: 12px; + color: #666; +} +#hideIgnoredTagsCb { + margin: 0 2px 0 1px; +} +#recaptcha_widget_div { + width: 318px; + float: left; + clear: both; +} +p.signup_p { + margin: 20px 0px 0px 0px; +} +.simple-subscribe-options ul { + list-style: none; + list-style-position: outside; + margin: 0; +} +/* a workaround to set link colors correctly */ +.wmd-preview a { + color: #1b79bd; +} +.wmd-preview li { + margin-bottom: 7px; + font-size: 14px; +} +.search-result-summary { + font-weight: bold; + font-size: 18px; + line-height: 22px; + margin: 0px 0px 0px 0px; + padding: 2px 0 0 0; + float: left; +} +.faq-rep-item { + text-align: right; + padding-right: 5px; +} +.user-info-table .gravatar { + margin: 0; +} +#responses { + clear: both; + line-height: 18px; + margin-bottom: 15px; +} +#responses div.face { + float: left; + text-align: center; + width: 54px; + padding: 3px; + overflow: hidden; +} +.response-parent { + margin-top: 18px; +} +.response-parent strong { + font-size: 20px; +} +.re { + min-height: 57px; + clear: both; + margin-top: 10px; +} +#responses input { + float: left; +} +#re_tools { + margin-bottom: 10px; +} +#re_sections { + margin-bottom: 6px; +} +#re_sections .on { + font-weight: bold; +} +.avatar-page ul { + list-style: none; +} +.avatar-page li { + display: inline; +} +.user-profile-page .avatar p { + margin-bottom: 0px; +} +.user-profile-page .tabBar a#stats { + margin-left: 0; +} +.user-profile-page img.gravatar { + margin: 2px 0 3px 0; +} +.user-profile-page h3 { + padding: 0; + margin-top: -3px; +} +.userList { + font-size: 13px; +} +img.flag { + border: 1px solid #eee; + vertical-align: text-top; +} +.main-page img.flag { + vertical-align: text-bottom; +} +/* Pretty printing styles. Used with prettify.js. */ +a.edit { + padding-left: 3px; + color: #145bff; +} +.str { + color: #080; +} +.kwd { + color: #008; +} +.com { + color: #800; +} +.typ { + color: #606; +} +.lit { + color: #066; +} +.pun { + color: #660; +} +.pln { + color: #000; +} +.tag { + color: #008; +} +/* name conflict here */ +.atn { + color: #606; +} +.atv { + color: #080; +} +.dec { + color: #606; +} +pre.prettyprint { + clear: both; + padding: 3px; + border: 0px solid #888; +} +@media print { + .str { + color: #060; + } + .kwd { + color: #006; + font-weight: bold; + } + .com { + color: #600; + font-style: italic; + } + .typ { + color: #404; + font-weight: bold; + } + .lit { + color: #044; + } + .pun { + color: #440; + } + .pln { + color: #000; + } + .tag { + color: #006; + font-weight: bold; + } + .atn { + color: #404; + } + .atv { + color: #060; + } +} +#leading-sidebar { + float: left; +} -- cgit v1.2.3-1-g7c22 From b9f2b91d1de36bf7ee3623cbf4a39610ffe36ab5 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 26 Jan 2012 19:35:23 -0300 Subject: added help page with the intention to eventually replace faq --- askbot/doc/source/changelog.rst | 1 + askbot/skins/default/templates/base.html | 2 +- askbot/skins/default/templates/help.html | 33 ++++++++++++++++++++++ askbot/skins/default/templates/widgets/footer.html | 2 ++ .../default/templates/widgets/user_navigation.html | 2 +- askbot/urls.py | 1 + askbot/views/meta.py | 7 +++++ 7 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 askbot/skins/default/templates/help.html diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index c6d29ea8..e2056b61 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -9,6 +9,7 @@ Development version (not released yet) * Askbot now respects django's staticfiles app (Radim Řehůřek, Evgeny) * Fixed the url translation bug (Evgeny) * Added left sidebar option (Evgeny) +* Added "help" page and links to in the header and the footer (Evgeny) 0.7.39 (Jan 11, 2012) --------------------- diff --git a/askbot/skins/default/templates/base.html b/askbot/skins/default/templates/base.html index 348ab23a..bd19f707 100644 --- a/askbot/skins/default/templates/base.html +++ b/askbot/skins/default/templates/base.html @@ -27,7 +27,7 @@ {% include "widgets/secondary_header.html" %} {# Scope selector, search input and ask button #} {% if settings.ENABLE_LEADING_SIDEBAR %}
- {{ settings.LEADING_SIDEBAR|safe }} + {{ settings.LEADING_SIDEBAR }}
{% endif %}
diff --git a/askbot/skins/default/templates/help.html b/askbot/skins/default/templates/help.html new file mode 100644 index 00000000..7dc58f5d --- /dev/null +++ b/askbot/skins/default/templates/help.html @@ -0,0 +1,33 @@ +{% extends "two_column_body.html" %} +{% block title %}{% trans %}Help{% endtrans %}{% endblock %} +{% block content %} +

{% trans %}Help{% endtrans %}

+

+ {% if request.user.is_authenticated() %} + {% trans username = request.user.username %}Welcome {{username}},{% endtrans %} + {% else %} + {% trans %}Welcome,{% endtrans %} + {% endif %} +

+

+ {% trans %}Thank you for using {{app_name}}, here is how it works.{% endtrans %} +

+

+ {% trans %}This site is for asking and answering questions, not for open-ended discussions.{% endtrans %} + {% trans %}We encourage everyone to use “question” space for asking and “answer” for answering.{% endtrans %} +

+

+ {% trans %}Despite that, each question and answer can be commented – + the comments are good for the limited discussions.{% endtrans %} +

+

+ {% trans %}Voting in {{app_name}} helps to select best answers and thank most helpful users.{% endtrans %} +

+ {% trans %}Please vote when you find helpful information, + it really helps the {{app_name}} community.{% endtrans %} + + {% trans %}Besides, you can @mention users anywhere in the text to point their attention, + follow users and conversations and report inappropriate content by flagging it.{% endtrans %} +

+

{% trans %}Enjoy.{% endtrans %}

+{% endblock %} diff --git a/askbot/skins/default/templates/widgets/footer.html b/askbot/skins/default/templates/widgets/footer.html index 14f18786..6eb3afc2 100644 --- a/askbot/skins/default/templates/widgets/footer.html +++ b/askbot/skins/default/templates/widgets/footer.html @@ -37,6 +37,8 @@