summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2012-01-26 23:17:13 -0300
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2012-01-26 23:17:13 -0300
commitf34d72018f9eb9dc8b3d5bf314477ab601d7fee5 (patch)
tree3459a0bdbd0dffae04666c5210c0ee49e732c04e
parentf52cd99b1abc88950c12d4b3fd47ade316ef6f81 (diff)
parentc9b50fd4536e6da56d28a0d53a4ce1b634c739e9 (diff)
downloadaskbot-f34d72018f9eb9dc8b3d5bf314477ab601d7fee5.tar.gz
askbot-f34d72018f9eb9dc8b3d5bf314477ab601d7fee5.tar.bz2
askbot-f34d72018f9eb9dc8b3d5bf314477ab601d7fee5.zip
Merge branch 'github-master'
-rwxr-xr-x.gitignore1
-rw-r--r--askbot/const/__init__.py3
-rw-r--r--askbot/deps/django_authopenid/migrations/0002_make_multiple_openids_possible.py7
-rw-r--r--askbot/locale/ca/LC_MESSAGES/django.mobin88690 -> 91439 bytes
-rw-r--r--askbot/locale/ca/LC_MESSAGES/django.po91
-rw-r--r--askbot/management/commands/merge_users.py8
-rw-r--r--askbot/migrations/0012_delete_some_unused_models.py8
-rw-r--r--askbot/migrations/0101_megadeath_of_q_a_c.py14
-rw-r--r--askbot/migrations/0102_rename_post_fields_back_1.py15
-rw-r--r--askbot/migrations/0103_rename_post_fields_back_2.py15
-rw-r--r--askbot/migrations/0104_auto__del_field_repute_question_post__add_field_repute_question.py15
-rw-r--r--askbot/migrations/0105_restore_anon_ans_q.py15
-rw-r--r--askbot/migrations/0106_update_postgres_full_text_setup.py3
-rw-r--r--askbot/migrations/0107_added_db_indexes.py280
-rw-r--r--askbot/migrations/__init__.py62
-rw-r--r--askbot/models/base.py7
-rw-r--r--askbot/models/post.py262
-rw-r--r--askbot/models/question.py191
-rw-r--r--askbot/models/tag.py37
-rw-r--r--askbot/search/postgresql/thread_and_post_models_01162012.plsql1
-rw-r--r--askbot/search/state_manager.py57
-rw-r--r--askbot/skins/default/media/style/style.css3670
-rw-r--r--askbot/skins/default/templates/main_page/questions_loop.html7
-rw-r--r--askbot/skins/default/templates/widgets/question_summary.html9
-rw-r--r--askbot/tests/page_load_tests.py204
-rw-r--r--askbot/tests/post_model_tests.py540
-rw-r--r--askbot/tests/search_state_tests.py120
-rw-r--r--askbot/views/commands.py10
-rw-r--r--askbot/views/readers.py37
29 files changed, 4622 insertions, 1067 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/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/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/locale/ca/LC_MESSAGES/django.mo b/askbot/locale/ca/LC_MESSAGES/django.mo
index 7904842d..71dbc41c 100644
--- a/askbot/locale/ca/LC_MESSAGES/django.mo
+++ b/askbot/locale/ca/LC_MESSAGES/django.mo
Binary files differ
diff --git a/askbot/locale/ca/LC_MESSAGES/django.po b/askbot/locale/ca/LC_MESSAGES/django.po
index 7f01961f..e7fd6aca 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
@@ -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 (<i>required</i>)"
-msgstr ""
+msgstr "Nom d'usuari (<i>requerit</i>)"
-# msgstr "El vostre nom d'usuari (<i>requerit</i>)"
#: 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/"
@@ -3106,11 +3101,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 +3684,6 @@ msgid "No email"
msgstr "Cap correu electrònic"
#: skins/common/templates/authopenid/authopenid_macros.html:53
-#, fuzzy
msgid "Please enter your <span>user name</span>, then sign in"
msgstr "Introduïu el vostre <span>nom d'usuari i contrasenya</span> per entrar"
@@ -4230,9 +4224,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 +4447,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 +4457,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 +4473,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 +4586,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 +5104,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 +5315,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 +5771,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 +5861,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 +5916,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
@@ -6151,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
@@ -6225,14 +6211,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"
@@ -6253,9 +6239,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 +6272,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"
@@ -6422,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
@@ -6485,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"
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 = '<from_user_id> <to_user_id>'
help = 'Merge an account and all information from a <user_id> to a <user_id>, deleting the <from_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 0f940b96..e788879c 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,8 @@ class Migration(DataMigration):
def forwards(self, orm):
"Write your forwards methods here."
+ return # TODO: remove me when the SQL is fixed!
+
if 'postgresql_psycopg2' in askbot.get_database_engine_name():
script_path = os.path.join(
askbot.get_install_directory(),
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/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)
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 d4482cf4..5cb9708f 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
@@ -382,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:
@@ -438,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)
@@ -450,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')
@@ -660,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()
@@ -682,12 +476,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 +510,6 @@ class Post(models.Model):
super(Post, self).delete(**kwargs)
-
-
def __unicode__(self):
if self.is_question():
return self.thread.title
@@ -733,12 +519,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 +871,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 +1035,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
@@ -1610,15 +1374,6 @@ class Post(models.Model):
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
raise NotImplementedError
@@ -1647,9 +1402,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()
diff --git a/askbot/models/question.py b/askbot/models/question.py
index 6f379491..6c2fa383 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -1,10 +1,12 @@
import datetime
import operator
+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
@@ -15,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):
@@ -93,42 +98,43 @@ 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(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,
}
- 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)
)
- 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
@@ -137,18 +143,19 @@ 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 = {}
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:
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:
@@ -184,7 +191,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
@@ -192,9 +198,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
@@ -214,47 +218,68 @@ 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):
+ # 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 = {}
+ 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
@@ -268,6 +293,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')
@@ -300,8 +327,14 @@ class Thread(models.Model):
class Meta:
app_label = 'askbot'
- def _question_post(self):
- return Post.objects.get(post_type='question', thread=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
+ 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()
@@ -318,6 +351,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
@@ -337,6 +373,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."
@@ -512,6 +551,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)
@@ -576,7 +619,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 <<<tag-name>>> into question title or body,
+ # then in html it'll become escaped like this: &lt;&lt;&lt;tag-name&gt;&gt;&gt;
+ 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 "<<<my-tag>>>"
+ 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/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/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/search/state_manager.py b/askbot/search/state_manager.py
index 211ce638..232d64e9 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):
@@ -209,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/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;
+}
diff --git a/askbot/skins/default/templates/main_page/questions_loop.html b/askbot/skins/default/templates/main_page/questions_loop.html
index 7e924e63..6a5e5e3d 100644
--- a/askbot/skins/default/templates/main_page/questions_loop.html
+++ b/askbot/skins/default/templates/main_page/questions_loop.html
@@ -1,9 +1,10 @@
{% 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)}} #}
+ {{ thread.get_summary_html(search_state=search_state) }}
{% endfor %}
-{% if questions.object_list|length == 0 %}
+{% if threads.object_list|length == 0 %}
{% include "main_page/nothing_found.html" %}
{% else %}
<div class="evenMore">
diff --git a/askbot/skins/default/templates/widgets/question_summary.html b/askbot/skins/default/templates/widgets/question_summary.html
index feebd27f..56154847 100644
--- a/askbot/skins/default/templates/widgets/question_summary.html
+++ b/askbot/skins/default/templates/widgets/question_summary.html
@@ -42,16 +42,17 @@
</div>
<div style="clear:both"></div>
<div class="userinfo">
- <span class="relativetime" title="{{thread.last_activity_at}}">{{ thread.last_activity_at|diff_date }}</span>
+ {# We have to kill microseconds below because InnoDB doesn't support them and all kinds of funny things happen in unit tests #}
+ <span class="relativetime" title="{{thread.last_activity_at.replace(microsecond=0)}}">{{ thread.last_activity_at|diff_date }}</span>
{% if question.is_anonymous %}
<span class="anonymous">{{ thread.last_activity_by.get_anonymous_name() }}</span>
{% else %}
- <a href="{% url user_profile thread.last_activity_by.id, thread.last_activity_by.username|slugify %}">{{thread.last_activity_by.username}}</a>{{ user_country_flag(thread.last_activity_by) }}
- {#{user_score_and_badge_summary(thread.last_activity_by)}#}
+ <a href="{% url user_profile thread.last_activity_by.id, thread.last_activity_by.username|slugify %}">{{thread.last_activity_by.username}}</a> {{ user_country_flag(thread.last_activity_by) }}
+ {#{user_score_and_badge_summary(thread.last_activity_by)}#}
{% endif %}
</div>
</div>
- <h2><a title="{{question.summary|escape}}" href="{{ question.get_absolute_url() }}">{{thread.get_title(question)|escape}}</a></h2>
+ <h2><a title="{{question.summary|escape}}" href="{{ question.get_absolute_url(thread=thread) }}">{{thread.get_title(question)|escape}}</a></h2>
{{ tag_list_widget(thread.get_tag_names(), search_state=search_state) }}
</div>
diff --git a/askbot/tests/page_load_tests.py b/askbot/tests/page_load_tests.py
index 16732e99..10bded11 100644
--- a/askbot/tests/page_load_tests.py
+++ b/askbot/tests/page_load_tests.py
@@ -1,18 +1,21 @@
-from django.test import TestCase
+from askbot.search.state_manager import SearchState
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
+from django.core.cache.backends.dummy import DummyCache
+from django.core import cache
+
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():
@@ -36,22 +39,48 @@ 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 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,
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:
@@ -79,16 +108,19 @@ 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')
@@ -99,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
@@ -170,76 +203,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..06bceca1 100644
--- a/askbot/tests/post_model_tests.py
+++ b/askbot/tests/post_model_tests.py
@@ -1,8 +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
+from askbot.models import Post, PostRevision, Thread, Tag
+from askbot.search.state_manager import DummySearchState
+from django.utils import simplejson
class PostModelTests(AskbotTestCase):
@@ -111,9 +122,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)
@@ -135,9 +146,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)
@@ -149,4 +160,517 @@ 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.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])
+
+ 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)
+
+
+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="<<<tag1>>> fake title", body_text="<<<tag2>>> <<<tag3>>> 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 `&lt;` was `&amp;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('&lt;&lt;&lt;tag1&gt;&gt;&gt; fake title' in proper_html)
+ #self.assertTrue('&lt;&lt;&lt;tag2&gt;&gt;&gt; &lt;&lt;&lt;tag3&gt;&gt;&gt; cheating' in proper_html)
+ self.assertFalse('<<<tag1>>>' in proper_html)
+ self.assertFalse('<<<tag2>>>' in proper_html)
+ self.assertFalse('<<<tag3>>>' 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('<<<tag1>>>', SearchState.get_empty().add_tag('tag1').full_url())\
+ .replace('<<<tag2>>>', SearchState.get_empty().add_tag('tag2').full_url())\
+ .replace('<<<tag3>>>', 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 <<<tag1>>>', timeout=100)
+
+ self.assertTrue(thread.summary_html_cached())
+ self.assertEqual('Test <<<tag1>>>', 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 <<<tag1>>>', timeout=100)
+
+ self.assertTrue(thread.summary_html_cached())
+ self.assertEqual('TestBBB <<<tag1>>>', 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 <<<tag2>>>"
+
+ 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
+ # 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)
+
+ 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()
+ # 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)
+
+ 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 - 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
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/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 1d592ab6..cccfce67 100644
--- a/askbot/views/readers.py
+++ b/askbot/views/readers.py
@@ -31,6 +31,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
@@ -38,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
@@ -72,24 +73,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 +105,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 +149,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 +162,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 +190,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,
@@ -207,6 +210,7 @@ def questions(request, **kwargs):
return render_into_skin('main_page.html', template_data, request)
+
def tags(request):#view showing a listing of available tags - plain list
tag_list_type = askbot_settings.TAG_LIST_FORMAT
@@ -452,6 +456,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