]> git.openstreetmap.org Git - osqa.git/commitdiff
Adds a new function in the profile menu for admins to suspend users, indefinetly...
authorhernani <hernani@0cfe37f9-358a-4d5e-be75-b63607b5c754>
Sat, 12 Jun 2010 01:16:53 +0000 (01:16 +0000)
committerhernani <hernani@0cfe37f9-358a-4d5e-be75-b63607b5c754>
Sat, 12 Jun 2010 01:16:53 +0000 (01:16 +0000)
git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@411 0cfe37f9-358a-4d5e-be75-b63607b5c754

26 files changed:
forum/actions/user.py
forum/http_responses.py [new file with mode: 0644]
forum/middleware/extended_user.py
forum/middleware/request_utils.py
forum/models/user.py
forum/skins/default/media/js/osqa.user.js
forum/skins/default/media/style/style.css
forum/skins/default/templates/403.html [new file with mode: 0644]
forum/skins/default/templates/404.html
forum/skins/default/templates/503.html [moved from forum/skins/default/templates/410.html with 100% similarity]
forum/skins/default/templates/node/comments.html
forum/skins/default/templates/node/contributors_info.html
forum/skins/default/templates/osqaadmin/djstyle_base.html
forum/skins/default/templates/osqaadmin/moderation.html [new file with mode: 0644]
forum/skins/default/templates/users/menu.html
forum/skins/default/templates/users/signature.html
forum/skins/default/templates/users/suspend_user.html [new file with mode: 0644]
forum/skins/default/templates/users/users.html
forum/templatetags/extra_tags.py
forum/urls.py
forum/views/admin.py
forum/views/auth.py
forum/views/commands.py
forum/views/decorators.py
forum/views/users.py
forum_modules/localauth/forms.py

index f76a281368c13df8e9fee0ed99225307d8c3d610..a3856a83bbd53d2dd5fbc011d56490b6678067d1 100644 (file)
@@ -102,4 +102,30 @@ class AwardAction(ActionProxy):
         'user': self.hyperlink(self.user.get_profile_url(), self.friendly_username(viewer, self.user)),
         'were_was': self.viewer_or_user_verb(viewer, self.user, _('were'), _('was')),
         'badge_name': self.award.badge.name,
+        }
+
+class SuspendAction(ActionProxy):
+    def process_data(self, **kwargs):
+        self.extra = kwargs
+
+    def process_action(self):
+        self.user.is_active = False
+        self.user.save()
+
+    def cancel_action(self):
+        self.user.is_active = True
+        self.user._pop_suspension_cache()
+        self.user.save()
+        self.user.message_set.create(message=_("Your suspension has been removed."))
+
+    def describe(self, viewer=None):
+        if self.extra.get('bantype', 'indefinitely') == 'forxdays' and self.extra.get('forxdays', None):
+            suspension = _("for %s days") % self.extra['forxdays']
+        else:
+            suspension = _("indefinetely")
+
+        return _("%(user)s %(were_was)s suspended %(suspension)s: %(msg)s") % {
+        'user': self.hyperlink(self.user.get_profile_url(), self.friendly_username(viewer, self.user)),
+        'were_was': self.viewer_or_user_verb(viewer, self.user, _('were'), _('was')),
+        'suspension': suspension, 'msg': self.extra.get('publicmsg', _('Bad behaviour'))
         }
\ No newline at end of file
diff --git a/forum/http_responses.py b/forum/http_responses.py
new file mode 100644 (file)
index 0000000..42bef58
--- /dev/null
@@ -0,0 +1,15 @@
+from django.http import HttpResponse
+from django.template.loader import render_to_string
+
+from forum import settings
+
+class HttpResponseServiceUnavailable(HttpResponse):
+    def __init__(self, message):
+        super(HttpResponseServiceUnavailable, self).__init__(content=render_to_string('503.html', {
+        'message': message,
+        'app_logo': settings.APP_LOGO,
+        'app_title': settings.APP_TITLE
+        }), status=503)
+
+class HttpResponseUnauthorized(HttpResponse):
+    pass
\ No newline at end of file
index e54c9b4aa50eeea05d80f7aac4daa98966e21704..0e44f736e2671b7cccf563e30c8ed54c1ec41e46 100644 (file)
@@ -1,12 +1,20 @@
 from django.contrib.auth.middleware import AuthenticationMiddleware\r
+from django.contrib.auth import logout\r
 from forum.models.user import AnonymousUser\r
+from forum.views.auth import forward_suspended_user\r
 \r
-class ExtendedUser(AuthenticationMiddleware):    \r
+class ExtendedUser(AuthenticationMiddleware):\r
     def process_request(self, request):\r
         super(ExtendedUser, self).process_request(request)\r
         if request.user.is_authenticated():\r
             try:\r
                 request.user = request.user.user\r
+\r
+                if request.user.is_suspended():\r
+                    user = request.user\r
+                    logout(request)\r
+                    return forward_suspended_user(request, user)\r
+\r
                 return None\r
             except:\r
                 pass\r
index 46d56b73f9add106b03b896ad8d6e75bec51fa27..40736bf7ff2ce1b88f5239741c0ec62e3943da88 100644 (file)
@@ -1,7 +1,6 @@
 from forum.settings import MAINTAINANCE_MODE, APP_LOGO, APP_TITLE\r
-from django.http import HttpResponseGone\r
-from django.template.loader import render_to_string\r
 \r
+from forum.http_responses import HttpResponseServiceUnavailable\r
 \r
 class RequestUtils(object):\r
     def __init__(self):\r
@@ -31,11 +30,7 @@ class RequestUtils(object):
             ip = request.META['REMOTE_ADDR']\r
 \r
             if not ip in MAINTAINANCE_MODE.value['allow_ips']:\r
-                return HttpResponseGone(render_to_string('410.html', {\r
-                    'message': MAINTAINANCE_MODE.value.get('message', ''),\r
-                    'app_logo': APP_LOGO,\r
-                    'app_title': APP_TITLE\r
-                }))\r
+                return HttpResponseServiceUnavailable(MAINTAINANCE_MODE.value.get('message', ''))\r
 \r
         if request.session.get('redirect_POST_data', None):\r
             request.POST = request.session.pop('redirect_POST_data')\r
index f027ffa350296f90f78f0644e0b0ebcacb53f26c..8837ef35836a22d84dd92e248aea51408d0f13d3 100644 (file)
@@ -1,4 +1,5 @@
 from base import *
+from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.auth.models import User as DjangoUser, AnonymousUser as DjangoAnonymousUser
 from django.db.models import Q
@@ -11,13 +12,12 @@ import string
 from random import Random
 
 from django.utils.translation import ugettext as _
-import django.dispatch
-
+import logging
 
 QUESTIONS_PER_PAGE_CHOICES = (
-   (10, u'10'),
-   (30, u'30'),
-   (50, u'50'),
+(10, u'10'),
+(30, u'30'),
+(50, u'50'),
 )
 
 class UserManager(CachedManager):
@@ -91,6 +91,7 @@ class AnonymousUser(DjangoAnonymousUser):
 def true_if_is_super_or_staff(fn):
     def decorated(self, *args, **kwargs):
         return self.is_superuser or self.is_staff or fn(self, *args, **kwargs)
+
     return decorated
 
 class User(BaseModel, DjangoUser):
@@ -101,7 +102,7 @@ class User(BaseModel, DjangoUser):
     gold = models.PositiveIntegerField(default=0)
     silver = models.PositiveIntegerField(default=0)
     bronze = models.PositiveIntegerField(default=0)
-    
+
     last_seen = models.DateTimeField(default=datetime.datetime.now)
     real_name = models.CharField(max_length=100, blank=True)
     website = models.URLField(max_length=200, blank=True)
@@ -113,7 +114,7 @@ class User(BaseModel, DjangoUser):
 
     vote_up_count = DenormalizedField("actions", canceled=False, action_type="voteup")
     vote_down_count = DenormalizedField("actions", canceled=False, action_type="votedown")
-   
+
     objects = UserManager()
 
     def __unicode__(self):
@@ -155,7 +156,7 @@ class User(BaseModel, DjangoUser):
         return self.get_profile_url()
 
     def get_profile_link(self):
-        profile_link = u'<a href="%s">%s</a>' % (self.get_profile_url(),self.username)
+        profile_link = u'<a href="%s">%s</a>' % (self.get_profile_url(), self.username)
         return mark_safe(profile_link)
 
     def get_visible_answers(self, question):
@@ -164,11 +165,12 @@ class User(BaseModel, DjangoUser):
     def get_vote_count_today(self):
         today = datetime.date.today()
         return self.actions.filter(canceled=False, action_type__in=("voteup", "votedown"),
-                action_date__gte=(today - datetime.timedelta(days=1))).count()
+                                   action_date__gte=(today - datetime.timedelta(days=1))).count()
 
     def get_reputation_by_upvoted_today(self):
         today = datetime.datetime.now()
-        sum = self.reputes.filter(reputed_at__range=(today - datetime.timedelta(days=1), today)).aggregate(models.Sum('value'))
+        sum = self.reputes.filter(reputed_at__range=(today - datetime.timedelta(days=1), today)).aggregate(
+                models.Sum('value'))
         #todo: redo this, maybe transform in the daily cap
         #if sum.get('value__sum', None) is not None: return sum['value__sum']
         return 0
@@ -176,7 +178,7 @@ class User(BaseModel, DjangoUser):
     def get_flagged_items_count_today(self):
         today = datetime.date.today()
         return self.actions.filter(canceled=False, action_type="flag",
-                action_date__gte=(today - datetime.timedelta(days=1))).count()
+                                   action_date__gte=(today - datetime.timedelta(days=1))).count()
 
     @true_if_is_super_or_staff
     def can_view_deleted_post(self, post):
@@ -204,7 +206,7 @@ class User(BaseModel, DjangoUser):
     @true_if_is_super_or_staff
     def can_comment(self, post):
         return self == post.author or self.reputation >= int(settings.REP_TO_COMMENT
-        ) or (post.__class__.__name__ == "Answer" and self == post.question.author)
+                                                             ) or (post.__class__.__name__ == "Answer" and self == post.question.author)
 
     @true_if_is_super_or_staff
     def can_like_comment(self, comment):
@@ -220,7 +222,8 @@ class User(BaseModel, DjangoUser):
         return self == comment.author or self.reputation >= int(settings.REP_TO_DELETE_COMMENTS)
 
     def can_convert_to_comment(self, answer):
-        return (not answer.marked) and (self.is_superuser or self.is_staff or answer.author == self or self.reputation >= int(settings.REP_TO_CONVERT_TO_COMMENT))
+        return (not answer.marked) and (self.is_superuser or self.is_staff or answer.author == self or self.reputation >= int
+                (settings.REP_TO_CONVERT_TO_COMMENT))
 
     @true_if_is_super_or_staff
     def can_accept_answer(self, answer):
@@ -233,7 +236,8 @@ class User(BaseModel, DjangoUser):
     @true_if_is_super_or_staff
     def can_edit_post(self, post):
         return self == post.author or self.reputation >= int(settings.REP_TO_EDIT_OTHERS
-        ) or (post.nis.wiki and self.reputation >= int(settings.REP_TO_EDIT_WIKI))
+                                                             ) or (post.nis.wiki and self.reputation >= int(
+                settings.REP_TO_EDIT_WIKI))
 
     @true_if_is_super_or_staff
     def can_wikify(self, post):
@@ -260,9 +264,9 @@ class User(BaseModel, DjangoUser):
     def can_delete_post(self, post):
         if post.node_type == "comment":
             return self.can_delete_comment(post)
-            
+
         return (self == post.author and (post.__class__.__name__ == "Answer" or
-            not post.answers.exclude(author=self).count()))
+        not post.answers.exclude(author=self).count()))
 
     @true_if_is_super_or_staff
     def can_upload_files(self):
@@ -272,6 +276,35 @@ class User(BaseModel, DjangoUser):
         self.__dict__.update(self.__class__.objects.filter(id=self.id).values('password')[0])
         return DjangoUser.check_password(self, old_passwd)
 
+    @property
+    def suspension(self):
+        if self.__dict__.get('_suspension_dencache_', False) != None:
+            try:
+                self.__dict__['_suspension_dencache_'] = self.actions.get(action_type="suspend", canceled=False)
+            except ObjectDoesNotExist:
+                self.__dict__['_suspension_dencache_'] = None
+            except MultipleObjectsReturned:
+                logging.error("Multiple suspension actions found for user %s (%s)" % (self.username, self.id))
+                self.__dict__['_suspension_dencache_'] = self.actions.filter(action_type="suspend", canceled=False
+                                                                             ).order_by('-action_date')[0]
+
+        return self.__dict__['_suspension_dencache_']
+
+    def _pop_suspension_cache(self):
+        self.__dict__.pop('_suspension_dencache_', None)
+
+    def is_suspended(self):
+        if not self.is_active:
+            suspension = self.suspension
+
+            if suspension and suspension.extra.get('bantype', None) == 'forxdays' and (
+            datetime.datetime.now() > suspension.action_date + datetime.timedelta(
+                    days=int(suspension.extra.get('forxdays', 365)))):
+                suspension.cancel()
+            else:
+                return True
+
+        return False
 
     class Meta:
         app_label = 'forum'
@@ -286,7 +319,7 @@ class SubscriptionSettings(models.Model):
     new_question = models.CharField(max_length=1, default='d')
     new_question_watched_tags = models.CharField(max_length=1, default='i')
     subscribed_questions = models.CharField(max_length=1, default='i')
-    
+
     #auto_subscribe_to
     all_questions = models.BooleanField(default=False)
     all_questions_watched_tags = models.BooleanField(default=False)
@@ -324,7 +357,7 @@ class ValidationHashManager(models.Manager):
             obj.save()
         except:
             return None
-            
+
         return obj
 
     def validate(self, hash, user, type, hash_data=[]):
@@ -352,7 +385,7 @@ class ValidationHashManager(models.Manager):
         return False
 
 class ValidationHash(models.Model):
-    hash_code = models.CharField(max_length=255,unique=True)
+    hash_code = models.CharField(max_length=255, unique=True)
     seed = models.CharField(max_length=12)
     expiration = models.DateTimeField(default=one_day_from_now)
     type = models.CharField(max_length=12)
@@ -368,7 +401,7 @@ class ValidationHash(models.Model):
         return self.hash_code
 
 class AuthKeyUserAssociation(models.Model):
-    key = models.CharField(max_length=255,null=False,unique=True)
+    key = models.CharField(max_length=255, null=False, unique=True)
     provider = models.CharField(max_length=64)
     user = models.ForeignKey(User, related_name="auth_keys")
     added_at = models.DateTimeField(default=datetime.datetime.now)
index 6450c3c50fb3712db1463f64c807468b13345934..5241880fde973fdbeb26774c75294ec92cef2f4e 100644 (file)
@@ -1,13 +1,13 @@
 $().ready(function() {
     var $dropdown = $('#user-menu-dropdown');
 
-    $('#user-menu').click(function(){
+    $('#user-menu').click(function() {
         $('.dialog').fadeOut('fast', function() {
             $dialog.remove();
         });
         $dropdown.slideToggle('fast', function() {
             if ($dropdown.is(':visible')) {
-               $dropdown.one('clickoutside', function() {
+                $dropdown.one('clickoutside', function() {
                     $dropdown.slideUp('fast')
                 });
             }
@@ -43,30 +43,31 @@ $().ready(function() {
                 + '<tr><th>' + messages.message + '</th><td><textarea id="award-message"></textarea></td></tr></table>';
 
         show_dialog({
-                html: table,
-                extra_class: 'award-rep-points',
-                event: e,
-                yes_callback: function($dialog) {
-                    var $points_input = $('#points-to-award');
-                    var _points = parseInt($points_input.val());
+            html: table,
+            extra_class: 'award-rep-points',
+            event: e,
+            yes_callback: function($dialog) {
+                var $points_input = $('#points-to-award');
+                var _points = parseInt($points_input.val());
 
-                    if(!isNaN(_points)) {
-                        $dialog.fadeOut('fast');
-                        var _message = $('#award-message').val();
-                        $.post($('#award-rep-points').attr('href'), {points: _points, message: _message}, function(data) {
-                            if (data.success) {
-                                $('#user-reputation').css('background', 'yellow');
-                                $('#user-reputation').html(data.reputation);
+                if (!isNaN(_points)) {
+                    $dialog.fadeOut('fast');
+                    var _message = $('#award-message').val();
+                    $.post($('#award-rep-points').attr('href'), {points: _points, message: _message}, function(data) {
+                        if (data.success) {
+                            $('#user-reputation').css('background', 'yellow');
+                            $('#user-reputation').html(data.reputation);
 
-                                $('#user-reputation').animate({ backgroundColor: "transparent" }, 1000);
+                            $('#user-reputation').animate({ backgroundColor: "transparent" }, 1000);
 
-                            }
-                        }, 'json')
-                    }
-                },
-                show_no: true
-            });
+                        }
+                    }, 'json')
+                }
+            },
+            show_no: true
+        });
 
         return false;
     });
+
 });
\ No newline at end of file
index 28bd8f34a29ce8c5a82f4ab1feb177185447a165..dc32123299c80642d5bdc35fab3a0620af30cb24 100644 (file)
 @import "jquery.autocomplete.css";
 
 body {
-       background: none repeat scroll 0 0 #FFFFFF;
-       color: #000000;
-       font-family: sans-serif;
-       font-size: 12px;
-       line-height: 150%;
-       margin: 0;
-       padding: 0;
+    background: none repeat scroll 0 0 #FFFFFF;
+    color: #000000;
+    font-family: sans-serif;
+    font-size: 12px;
+    line-height: 150%;
+    margin: 0;
+    padding: 0;
 }
 
 div {
-       margin: 0 auto;
-       padding: 0;
+    margin: 0 auto;
+    padding: 0;
 }
 
-h1,h2,h3,ul,li,form,img,p {
-       border: medium none;
-       margin: 0;
-       padding: 0;
+h1, h2, h3, ul, li, form, img, p {
+    border: medium none;
+    margin: 0;
+    padding: 0;
 }
 
-label {vertical-align: middle;}
+label {
+    vertical-align: middle;
+}
 
-.login label {display: block;}
-.login .form-row-vertical {margin-bottom: 8px;}
+.login label {
+    display: block;
+}
+
+.login .form-row-vertical {
+    margin-bottom: 8px;
+}
 
 hr {
-       border-color: #CCCCCE -moz-use-text-color -moz-use-text-color;
-       border-right: medium none;
-       border-style: dashed none none;
-       border-width: 1px medium medium;
+    border-color: #CCCCCE -moz-use-text-color -moz-use-text-color;
+    border-right: medium none;
+    border-style: dashed none none;
+    border-width: 1px medium medium;
 }
 
-input,select {
-       font-family: Trebuchet MS,"segoe ui",Helvetica,"Microsoft YaHei",Tahoma,Verdana,MingLiu,PMingLiu,Arial,sans-serif;
-       vertical-align: middle;
+input, select {
+    font-family: Trebuchet MS, "segoe ui", Helvetica, "Microsoft YaHei", Tahoma, Verdana, MingLiu, PMingLiu, Arial, sans-serif;
+    vertical-align: middle;
 }
 
 p {
-       font-size: 13px;
-       line-height: 140%;
-       margin-bottom: 13px;
+    font-size: 13px;
+    line-height: 140%;
+    margin-bottom: 13px;
 }
 
 a {
-       color: #3060A8;
-       text-decoration: none;
+    color: #3060A8;
+    text-decoration: none;
 }
 
 .badges a {
-       color: #763333;
-       text-decoration: underline;
+    color: #763333;
+    text-decoration: underline;
+}
+
+a:hover {
+    text-decoration: underline;
 }
 
-a:hover { text-decoration: underline; }
-.tright { text-align: right; }
+.tright {
+    text-align: right;
+}
 
 .spacer3 {
-       clear: both;
-       height: 30px;
-       line-height: 30px;
-       visibility: hidden;
+    clear: both;
+    height: 30px;
+    line-height: 30px;
+    visibility: hidden;
 }
 
 h1 {
-       font-size: 160%;
-       padding: 5px 0;
+    font-size: 160%;
+    padding: 5px 0;
     line-height: 110%;
 }
 
 h2 {
-       font-size: 140%;
-       padding: 3px 0;
+    font-size: 140%;
+    padding: 3px 0;
     line-height: 110%;
 }
 
 h3 {
-       font-size: 120%;
-       padding: 3px 0;
-    line-height: 110%;    
+    font-size: 120%;
+    padding: 3px 0;
+    line-height: 110%;
 }
 
 ul {
-       list-style: disc outside none;
-       margin-bottom: 1em;
-       margin-left: 20px;
-       padding-left: 0;
+    list-style: disc outside none;
+    margin-bottom: 1em;
+    margin-left: 20px;
+    padding-left: 0;
 }
 
 ol {
-       list-style: decimal outside none;
-       margin-bottom: 1em;
-       margin-left: 30px;
-       padding-left: 0;
+    list-style: decimal outside none;
+    margin-bottom: 1em;
+    margin-left: 30px;
+    padding-left: 0;
+}
+
+td ul {
+    vertical-align: middle;
 }
 
-td ul { vertical-align: middle; }
-li input { margin: 3px 3px 4px; }
+li input {
+    margin: 3px 3px 4px;
+}
 
 pre {
-       background-color: #F5F5F5;
-       font-family: Consolas,Monaco,Liberation Mono,Lucida Console,Monospace;
-       font-size: 100%;
-       margin-bottom: 10px;
-       overflow: auto;
-       padding-left: 5px;
-       padding-top: 5px;
-       width: 580px;
+    background-color: #F5F5F5;
+    font-family: Consolas, Monaco, Liberation Mono, Lucida Console, Monospace;
+    font-size: 100%;
+    margin-bottom: 10px;
+    overflow: auto;
+    padding-left: 5px;
+    padding-top: 5px;
+    width: 580px;
 }
 
 code {
-       font-family: Consolas,Monaco,Liberation Mono,Lucida Console,Monospace;
-       font-size: 100%;
+    font-family: Consolas, Monaco, Liberation Mono, Lucida Console, Monospace;
+    font-size: 100%;
 }
 
 blockquote {
-       background-color: #F5F5F5;
-       margin-bottom: 10px;
-       margin-right: 15px;
-       padding: 10px 0 1px 10px;
+    background-color: #F5F5F5;
+    margin-bottom: 10px;
+    margin-right: 15px;
+    padding: 10px 0 1px 10px;
 }
 
 #wrapper {
-       margin: auto;
-       padding: 0;
-       width: 990px;
+    margin: auto;
+    padding: 0;
+    width: 990px;
 }
 
 #roof {
-       background: none repeat scroll 0 0 #FFFFFF;
-       margin-top: 0;
-       position: relative;
+    background: none repeat scroll 0 0 #FFFFFF;
+    margin-top: 0;
+    position: relative;
 }
 
 #room {
-       background-color: #FFFFFF;
-       border-bottom: 1px solid #777777;
-       padding: 10px 0;
+    background-color: #FFFFFF;
+    border-bottom: 1px solid #777777;
+    padding: 10px 0;
 }
 
 #CALeft {
-       float: left;
-       position: relative;
-       width: 740px;
+    float: left;
+    position: relative;
+    width: 740px;
 }
 
 #CARight {
-       float: right;
-       width: 240px;
+    float: right;
+    width: 240px;
 }
 
 #CAFull {
-       float: left;
-       padding: 0 5px;
-       width: 950px;
+    float: left;
+    padding: 0 5px;
+    width: 950px;
 }
 
 #ground {
-       background: none repeat scroll 0 0 #BDCCED;
-       border-top: 1px solid #000000;
-       padding-bottom: 0;
-       padding-top: 6px;
-       text-align: center;
-       width: 100%;
+    background: none repeat scroll 0 0 #BDCCED;
+    border-top: 1px solid #000000;
+    padding-bottom: 0;
+    padding-top: 6px;
+    text-align: center;
+    width: 100%;
 }
 
 #top {
-       background-color: #FFFFFF;
-       height: 20px;
-       padding: 3px;
-       position: absolute;
-       right: 0;
-       text-align: right;
-       top: 0;
-       width: 500px;
+    background-color: #FFFFFF;
+    height: 20px;
+    padding: 3px;
+    position: absolute;
+    right: 0;
+    text-align: right;
+    top: 0;
+    width: 500px;
 }
 
 #top a {
-       color: #333333;
-       font-size: 12px;
-       height: 35px;
-       margin-left: 20px;
-       text-align: right;
-       text-decoration: underline;
+    color: #333333;
+    font-size: 12px;
+    height: 35px;
+    margin-left: 20px;
+    text-align: right;
+    text-decoration: underline;
 }
 
-#logo { padding: 5px 0 0; }
+#logo {
+    padding: 5px 0 0;
+}
 
 #navBar {
-       display: block;
-       position: relative;
-       width: 990px;
+    display: block;
+    position: relative;
+    width: 990px;
 }
 
-#navBar .nav { margin: 20px 0 0 16px; }
+#navBar .nav {
+    margin: 20px 0 0 16px;
+}
 
 #navBar .nav a {
-       background-color: #E5EBF8;
-       border-color: #888888 #888888 -moz-use-text-color;
-       border-style: solid solid none;
-       border-width: 1px 1px medium;
-       color: #333333;
-       display: block;
-       float: left;
-       font-size: 14px;
-       font-weight: 400;
-       height: 25px;
-       line-height: 30px;
-       margin-left: 10px;
-       padding: 0 12px 3px;
-       text-decoration: none;
-}
-
-#navBar .nav a:hover { text-decoration: underline; }
+    background-color: #E5EBF8;
+    border-color: #888888 #888888 -moz-use-text-color;
+    border-style: solid solid none;
+    border-width: 1px 1px medium;
+    color: #333333;
+    display: block;
+    float: left;
+    font-size: 14px;
+    font-weight: 400;
+    height: 25px;
+    line-height: 30px;
+    margin-left: 10px;
+    padding: 0 12px 3px;
+    text-decoration: none;
+}
+
+#navBar .nav a:hover {
+    text-decoration: underline;
+}
 
 #navBar .nav a.on {
-       background: none repeat scroll 0 0 #356FCB;
-       border: 1px solid #356FCB;
-       color: #FFFFFF;
-       font-weight: 600;
-       height: 24px;
-       line-height: 28px;
-       text-decoration: none;
+    background: none repeat scroll 0 0 #356FCB;
+    border: 1px solid #356FCB;
+    color: #FFFFFF;
+    font-weight: 600;
+    height: 24px;
+    line-height: 28px;
+    text-decoration: none;
 }
 
 #navBar .nav a.special {
-       color: #B02B2C;
-       font-size: 14px;
-       font-weight: bold;
-       text-decoration: none;
+    color: #B02B2C;
+    font-size: 14px;
+    font-weight: bold;
+    text-decoration: none;
 }
 
-#navBar .nav a.special:hover { text-decoration: underline; }
+#navBar .nav a.special:hover {
+    text-decoration: underline;
+}
 
 #navBar .nav div.focus {
-       float: right;
-       padding-right: 0;
+    float: right;
+    padding-right: 0;
 }
 
 #searchBar {
-       background-color: #B6C4E2;
-       border-color: #EEEEEC #A9ACA5 #BABDB6 #EEEEEC;
-       border-style: solid;
-       border-width: 1px;
-       padding: 4px 0 0;
-       width: 988px;
+    background-color: #B6C4E2;
+    border-color: #EEEEEC #A9ACA5 #BABDB6 #EEEEEC;
+    border-style: solid;
+    border-width: 1px;
+    padding: 4px 0 0;
+    width: 988px;
 }
 
-#searchBar .content { }
+#searchBar .content {
+}
 
 #searchBar .searchInput {
-       font-size: 13px;
-       height: 18px;
-       width: 400px;
+    font-size: 13px;
+    height: 18px;
+    width: 400px;
 }
 
 #searchBar .searchBtn {
-       font-size: 14px;
-       height: 26px;
-       width: 80px;
+    font-size: 14px;
+    height: 26px;
+    width: 80px;
 }
 
 #searchBar .options {
-       color: #333333;
-       font-size: 120%;
-       padding: 3px 0;
+    color: #333333;
+    font-size: 120%;
+    padding: 3px 0;
+}
+
+#searchBar .options input {
+    margin: 0 3px 0 15px;
 }
 
-#searchBar .options input { margin: 0 3px 0 15px; }
-#searchBar .options input:hover { cursor: pointer; }
+#searchBar .options input:hover {
+    cursor: pointer;
+}
 
 #listA {
-       background-color: #FFFFFF;
-       float: left;
-       padding: 0 0;
-       width: 100%;
+    background-color: #FFFFFF;
+    float: left;
+    padding: 0 0;
+    width: 100%;
 }
 
 .thousand {
-       color: orange;
+    color: orange;
 }
 
 .short-summary {
-       border-top: 1px dotted #CCCCCE;
-       float: left;
-       overflow: hidden;
-       padding: 3px 0px 5px 0;
-       position: relative;
-       width: 740px;
+    border-top: 1px dotted #CCCCCE;
+    float: left;
+    overflow: hidden;
+    padding: 3px 0px 5px 0;
+    position: relative;
+    width: 740px;
 }
 
 .short-summary h2 a {
-       color: #2A5594;
-       font-family: "Trebuchet MS","segoe ui",arial,sans-serif;
-       font-size: 17px;
+    color: #2A5594;
+    font-family: "Trebuchet MS", "segoe ui", arial, sans-serif;
+    font-size: 17px;
 }
 
 .short-summary .userinfo {
-       color: #666666;
-       float: right;
-       margin-top: 8px;
+    color: #666666;
+    float: right;
+    margin-top: 8px;
 }
 
-.userinfo a,a.userinfo { color: #3060A8; }
+.userinfo a, a.userinfo {
+    color: #3060A8;
+}
 
 .short-summary .counts {
-       float: left;
-       margin-right: 0px;
-       margin-top: 4px;
-       padding-right: 2px;
+    float: left;
+    margin-right: 0px;
+    margin-top: 4px;
+    padding-right: 2px;
 }
 
 .short-summary .counts .item-count {
-       font-size: 17px;
-       font-weight: bold;
+    font-size: 17px;
+    font-weight: bold;
 }
 
-.short-summary .votes,.short-summary .status,.short-summary .views {
-       -moz-border-radius: 5px 5px 5px 5px;
-       border-bottom: 1px solid #CCCCCC;
-       border-right: 1px solid #CCCCCC;
-       float: left;
-       font-size: 11px;
-       height: 42px;
-       margin: 0 6px 0 0px;
-       padding: 8px 2px 0;
-       text-align: center;
-       width: 46px;
+.short-summary .votes, .short-summary .status, .short-summary .views {
+    -moz-border-radius: 5px 5px 5px 5px;
+    border-bottom: 1px solid #CCCCCC;
+    border-right: 1px solid #CCCCCC;
+    float: left;
+    font-size: 11px;
+    height: 42px;
+    margin: 0 6px 0 0px;
+    padding: 8px 2px 0;
+    text-align: center;
+    width: 46px;
 }
 
-.short-summary .votes,.short-summary .views { color: #666666; }
+.short-summary .votes, .short-summary .views {
+    color: #666666;
+}
 
 .short-summary .favorites {
-       width: 24px;
-       float: left;
-       text-align: center;
+    width: 24px;
+    float: left;
+    text-align: center;
 }
 
-#question-table { margin-bottom: 10px; }
+#question-table {
+    margin-bottom: 10px;
+}
 
 .questions-count {
-       color: #A40000;
-       font-family: sans-serif;
-       font-size: 24px;
-       font-weight: 600;
-       margin-top: 3px;
+    color: #A40000;
+    font-family: sans-serif;
+    font-size: 24px;
+    font-weight: 600;
+    margin-top: 3px;
     margin-right: 5px;
-       padding: 0 0 5px 0;
+    padding: 0 0 5px 0;
 }
 
 .boxA {
@@ -357,819 +394,975 @@ blockquote {
 }
 
 .boxA h3 {
-       color: #FFFFFF;
-       font-size: 13px;
-       font-weight: 800;
-       margin: 0 0 4px;
-       padding: 0;
+    color: #FFFFFF;
+    font-size: 13px;
+    font-weight: 800;
+    margin: 0 0 4px;
+    padding: 0;
 }
 
 .boxA .body {
-       background: none repeat scroll 0 0 #FFFFFF;
-       border: 1px solid #999999;
-       font-size: 13px;
-       padding: 8px;
+    background: none repeat scroll 0 0 #FFFFFF;
+    border: 1px solid #999999;
+    font-size: 13px;
+    padding: 8px;
 }
 
 .boxA .more {
-       font-weight: 800;
-       padding: 2px;
-       text-align: right;
+    font-weight: 800;
+    padding: 2px;
+    text-align: right;
 }
 
 .boxC {
-       background: none repeat scroll 0 0 #E5EBF8;
-       border-color: #EEEEEC #A9ACA5 #BABDB6 #EEEEEC;
-       border-style: solid;
-       border-width: 1px;
-       margin-bottom: 8px;
-       padding: 10px;
+    background: none repeat scroll 0 0 #E5EBF8;
+    border-color: #EEEEEC #A9ACA5 #BABDB6 #EEEEEC;
+    border-style: solid;
+    border-width: 1px;
+    margin-bottom: 8px;
+    padding: 10px;
+}
+
+.boxC p {
+    margin-bottom: 8px;
 }
 
-.boxC p { margin-bottom: 8px; }
-.boxC p.nomargin { margin: 0; }
+.boxC p.nomargin {
+    margin: 0;
+}
 
 .boxC p.info-box-follow-up-links {
-       margin: 0;
-       text-align: right;
+    margin: 0;
+    text-align: right;
 }
 
 .pager {
-       float: left;
-       margin-bottom: 16px;
-       margin-top: 10px;
+    float: left;
+    margin-bottom: 16px;
+    margin-top: 10px;
 }
 
 .pagesize {
-       float: right;
-       margin-bottom: 16px;
-       margin-top: 10px;
+    float: right;
+    margin-bottom: 16px;
+    margin-top: 10px;
 }
 
 .paginator {
-       font: 12px sans-serif;
-       padding: 5px 0 10px;
+    font: 12px sans-serif;
+    padding: 5px 0 10px;
+}
+
+.paginator .prev a, .paginator .prev a:visited, .paginator .next a, .paginator .next a:visited {
+    background-color: #FFFFFF;
+    border: 1px solid #FFFFFF;
+    color: #777777;
+    font: bold 100% sans-serif;
+    padding: 2px 4px 3px;
 }
 
-.paginator .prev a,.paginator .prev a:visited,.paginator .next a,.paginator .next a:visited {
-       background-color: #FFFFFF;
-       border: 1px solid #FFFFFF;
-       color: #777777;
-       font: bold 100% sans-serif;
-       padding: 2px 4px 3px;
+.paginator .prev {
+    margin-right: 0.5em;
 }
 
-.paginator .prev { margin-right: 0.5em; }
-.paginator .next { margin-left: 0.5em; }
+.paginator .next {
+    margin-left: 0.5em;
+}
 
-.paginator .page a,.paginator .page a:visited,.paginator .curr {
-       background-color: #FFFFFF;
-       border: 1px solid #CCCCCC;
-       color: #777777;
-       font: 0.875em verdana;
-       margin: 0 0.25em;
-       padding: 0.25em;
+.paginator .page a, .paginator .page a:visited, .paginator .curr {
+    background-color: #FFFFFF;
+    border: 1px solid #CCCCCC;
+    color: #777777;
+    font: 0.875em verdana;
+    margin: 0 0.25em;
+    padding: 0.25em;
 }
 
 .paginator .curr {
-       background-color: #777777;
-       border: 1px solid #777777;
-       color: #FFFFFF;
-       font-weight: bold;
+    background-color: #777777;
+    border: 1px solid #777777;
+    color: #FFFFFF;
+    font-weight: bold;
 }
 
-.paginator .page a:hover,.paginator .prev a:hover,.paginator .next a:hover {
-       background-color: #777777;
-       border: 1px solid #777777;
-       color: #FFFFFF;
-       text-decoration: none;
+.paginator .page a:hover, .paginator .prev a:hover, .paginator .next a:hover {
+    background-color: #777777;
+    border: 1px solid #777777;
+    color: #FFFFFF;
+    text-decoration: none;
 }
 
 .paginator .text {
-       color: #777777;
-       font: bold 100% sans-serif;
-       padding: 0.3em;
+    color: #777777;
+    font: bold 100% sans-serif;
+    padding: 0.3em;
 }
 
-.paginator-container-left { padding: 5px 0 10px; }
+.paginator-container-left {
+    padding: 5px 0 10px;
+}
 
 .tags {
-       display: block;
-       font-family: sans-serif;
-       line-height: 200%;
-       margin-top: 5px;
+    display: block;
+    font-family: sans-serif;
+    line-height: 200%;
+    margin-top: 5px;
 }
 
-.tags a,span.tag {
-       background-color: #EEEEEE;
-       border-bottom: 1px solid #CCCCCC;
-       border-right: 1px solid #CCCCCC;
-       color: #777777;
-       font-size: 11px;
-       font-weight: normal;
-       padding: 1px 8px;
-       text-decoration: none;
-       white-space: nowrap;
+.tags a, span.tag {
+    background-color: #EEEEEE;
+    border-bottom: 1px solid #CCCCCC;
+    border-right: 1px solid #CCCCCC;
+    color: #777777;
+    font-size: 11px;
+    font-weight: normal;
+    padding: 1px 8px;
+    text-decoration: none;
+    white-space: nowrap;
 }
 
 .tags a:hover {
-       background-color: #356FCB;
-       color: #FFFFFF;
+    background-color: #356FCB;
+    color: #FFFFFF;
 }
 
 .tag-number {
-       font-family: sans-serif;
-       font-weight: 700;
+    font-family: sans-serif;
+    font-weight: 700;
 }
 
 .marked-tags {
-       margin-bottom: 5px;
-       margin-top: 0;
+    margin-bottom: 5px;
+    margin-top: 0;
 }
 
 a.medal {
-       background: none repeat scroll 0 0 #FFFFCD;
-       border-color: #EEEEEE #CCCCCC #CCCCCC #EEEEEE;
-       border-left: 1px solid #EEEEEE;
-       border-style: solid;
-       border-width: 1px;
-       color: #333333;
-       font-size: 14px;
-       font-weight: bold;
-       line-height: 250%;
-       padding: 4px 12px 4px 6px;
-       text-decoration: none;
+    background: none repeat scroll 0 0 #FFFFCD;
+    border-color: #EEEEEE #CCCCCC #CCCCCC #EEEEEE;
+    border-left: 1px solid #EEEEEE;
+    border-style: solid;
+    border-width: 1px;
+    color: #333333;
+    font-size: 14px;
+    font-weight: bold;
+    line-height: 250%;
+    padding: 4px 12px 4px 6px;
+    text-decoration: none;
 }
 
 a.medal:hover {
-       background: url("../images/medala_on.gif") no-repeat scroll 0 0 transparent;
-       border-color: #E7E296 #D1CA3D #D1CA3D #E7E296;
-       border-left: 1px solid #E7E296;
-       border-style: solid;
-       border-width: 1px;
-       color: #333333;
-       text-decoration: none;
+    background: url("../images/medala_on.gif") no-repeat scroll 0 0 transparent;
+    border-color: #E7E296 #D1CA3D #D1CA3D #E7E296;
+    border-left: 1px solid #E7E296;
+    border-style: solid;
+    border-width: 1px;
+    color: #333333;
+    text-decoration: none;
 }
 
 .tabBar {
-       background-color: #FFFFFF;
-       border-bottom: 1px solid white;
-       clear: both;
-       height: 30px;
-       margin-bottom: 3px;
-       width: 100%;
+    background-color: #FFFFFF;
+    border-bottom: 1px solid white;
+    clear: both;
+    height: 30px;
+    margin-bottom: 3px;
+    width: 100%;
 }
 
 .tabsA {
-       background-color: #FFFFFF;
-       display: block;
-       float: right;
-       font-weight: bold;
-       height: 20px;
-       position: relative;
+    background-color: #FFFFFF;
+    display: block;
+    float: right;
+    font-weight: bold;
+    height: 20px;
+    position: relative;
 }
 
 .tabsA a {
-       background: none repeat scroll 0 0 #EEEEEE;
-       border-bottom: 1px solid #CCCCCC;
-       border-right: 1px solid #CCCCCC;
-       color: #888A85;
-       display: block;
-       float: left;
-       height: 20px;
-       line-height: 22px;
-       margin: 5px 4px 0 0;
-       padding: 0 11px;
-       text-decoration: none;
+    background: none repeat scroll 0 0 #EEEEEE;
+    border-bottom: 1px solid #CCCCCC;
+    border-right: 1px solid #CCCCCC;
+    color: #888A85;
+    display: block;
+    float: left;
+    height: 20px;
+    line-height: 22px;
+    margin: 5px 4px 0 0;
+    padding: 0 11px;
+    text-decoration: none;
 }
 
-.tabsA a.on,.tabsA a:hover {
-       background: none repeat scroll 0 0 #FFFFFF;
-       color: #A40000;
+.tabsA a.on, .tabsA a:hover {
+    background: none repeat scroll 0 0 #FFFFFF;
+    color: #A40000;
 }
 
 .tabsA a:hover {
-       background: none repeat scroll 0 0 #356FCB;
-       color: #FFFFFF;
+    background: none repeat scroll 0 0 #356FCB;
+    color: #FFFFFF;
 }
 
 .headlineA {
-       border-bottom: 1px solid #777777;
-       font-size: 13px;
-       font-weight: 800;
-       height: 30px;
-       margin-bottom: 12px;
-       padding-bottom: 2px;
-       text-align: right;
+    border-bottom: 1px solid #777777;
+    font-size: 13px;
+    font-weight: 800;
+    height: 30px;
+    margin-bottom: 12px;
+    padding-bottom: 2px;
+    text-align: right;
 }
 
 .headQuestions {
-       background: url("../images/dot-list.gif") no-repeat scroll left center transparent;
-       border-bottom: 0 solid #777777;
-       float: left;
-       font-size: 15px;
-       font-weight: 700;
-       height: 23px;
-       line-height: 23px;
-       margin: 5px 0 0 5px;
-       padding: 0 6px 0 15px;
+    background: url("../images/dot-list.gif") no-repeat scroll left center transparent;
+    border-bottom: 0 solid #777777;
+    float: left;
+    font-size: 15px;
+    font-weight: 700;
+    height: 23px;
+    line-height: 23px;
+    margin: 5px 0 0 5px;
+    padding: 0 6px 0 15px;
 }
 
 .headUsers {
-       background: url("../images/dot-list.gif") no-repeat scroll left center transparent;
-       border-bottom: 0 solid #777777;
-       float: left;
-       font-size: 15px;
-       font-weight: 700;
-       height: 23px;
-       line-height: 23px;
-       margin: 5px 0 0 5px;
-       padding: 0 6px 0 15px;
+    background: url("../images/dot-list.gif") no-repeat scroll left center transparent;
+    border-bottom: 0 solid #777777;
+    float: left;
+    font-size: 15px;
+    font-weight: 700;
+    height: 23px;
+    line-height: 23px;
+    margin: 5px 0 0 5px;
+    padding: 0 6px 0 15px;
 }
 
 .headMedals {
-       background: url("../images/dot-list.gif") no-repeat scroll left center transparent;
-       border-bottom: 0 solid #777777;
-       float: left;
-       font-size: 15px;
-       font-weight: 700;
-       height: 23px;
-       line-height: 23px;
-       margin: 5px 0 0 5px;
-       padding: 0 6px 0 15px;
+    background: url("../images/dot-list.gif") no-repeat scroll left center transparent;
+    border-bottom: 0 solid #777777;
+    float: left;
+    font-size: 15px;
+    font-weight: 700;
+    height: 23px;
+    line-height: 23px;
+    margin: 5px 0 0 5px;
+    padding: 0 6px 0 15px;
 }
 
 .headNormal {
-       border-bottom: 1px solid #777777;
-       font-size: 15px;
-       font-weight: bold;
-       margin-bottom: 12px;
-       padding: 3px;
-       text-align: left;
+    border-bottom: 1px solid #777777;
+    font-size: 15px;
+    font-weight: bold;
+    margin-bottom: 12px;
+    padding: 3px;
+    text-align: left;
 }
 
 .headUser {
-       border-bottom: 1px solid #777777;
-       font-size: 20px;
-       font-weight: 800;
-       margin-bottom: 12px;
-       padding: 5px;
-       text-align: left;
+    border-bottom: 1px solid #777777;
+    font-size: 20px;
+    font-weight: 800;
+    margin-bottom: 12px;
+    padding: 5px;
+    text-align: left;
 }
 
 .questions-related {
-       font-weight: 700;
-       word-wrap: break-word;
+    font-weight: 700;
+    word-wrap: break-word;
 }
 
 .questions-related p {
-       font-size: 100%;
-       line-height: 20px;
-       margin-bottom: 10px;
+    font-size: 100%;
+    line-height: 20px;
+    margin-bottom: 10px;
 }
 
 .question-body {
-       font-size: 13px;
-       line-height: 20px;
-       min-height: 100px;
+    font-size: 13px;
+    line-height: 20px;
+    min-height: 100px;
 }
 
-.question-body img { max-width: 640px; }
+.question-body img {
+    max-width: 640px;
+}
 
 .vote-buttons {
-       float: left;
-       text-align: center;
+    float: left;
+    text-align: center;
 }
 
 span.form-error {
-       color: #990000;
-       font-weight: normal;
-       margin-left: 5px;
+    color: #990000;
+    font-weight: normal;
+    margin-left: 5px;
 }
 
 ul.errorlist li {
-       color: #990000;
-       font-weight: normal;
-       margin-left: 0px;
+    color: #990000;
+    font-weight: normal;
+    margin-left: 0px;
     margin-top: 5px;
 }
 
 .answer {
-       border-bottom: 1px solid #CCCCCE;
-       padding-top: 10px;
-       width: 100%;
+    border-bottom: 1px solid #CCCCCE;
+    padding-top: 10px;
+    width: 100%;
 }
 
 .answer-body {
-       font-size: 13px;
-       line-height: 20px;
-       min-height: 80px;
+    font-size: 13px;
+    line-height: 20px;
+    min-height: 80px;
 }
 
-.answer-body img { max-width: 640px; }
+.answer-body img {
+    max-width: 640px;
+}
 
-.answered-by-owner { background: none repeat scroll 0 0 #E9E9FF; }
+.answered-by-owner {
+    background: none repeat scroll 0 0 #E9E9FF;
+}
 
 .accepted-answer {
-       background-color: #EBFFE6;
-       border-bottom-color: #9BD59B;
+    background-color: #EBFFE6;
+    border-bottom-color: #9BD59B;
 }
 
 .answered {
-       background: none repeat scroll 0 0 #E5EBF8;
-       color: #314362;
+    background: none repeat scroll 0 0 #E5EBF8;
+    color: #314362;
 }
 
-.answered-accepted,.answer-votes.answered-accepted {
-       background: none repeat scroll 0 0 #E6F8DD;
-       color: #3A6231;
+.answered-accepted, .answer-votes.answered-accepted {
+    background: none repeat scroll 0 0 #E6F8DD;
+    color: #3A6231;
 }
 
 .unanswered {
-       background: none repeat scroll 0 0 #F3E3E1;
-       color: #6B2B28;
+    background: none repeat scroll 0 0 #F3E3E1;
+    color: #6B2B28;
 }
 
 .tagsList {
-       list-style-type: none;
-       margin: 0;
-       min-height: 360px;
-       padding: 0;
+    list-style-type: none;
+    margin: 0;
+    min-height: 360px;
+    padding: 0;
 }
 
 .tagsList li {
-       float: left;
-       width: 235px;
+    float: left;
+    width: 235px;
 }
 
 .badge-list {
-       list-style-type: none;
-       margin: 0;
+    list-style-type: none;
+    margin: 0;
+}
+
+.badge-list a {
+    color: #3060A8;
 }
 
-.badge-list a { color: #3060A8; }
-.badge-list a.medal { color: #333333; }
-.list-item { margin-left: 15px; }
+.badge-list a.medal {
+    color: #333333;
+}
+
+.list-item {
+    margin-left: 15px;
+}
 
 .list-item li {
-       font-size: 13px;
-       line-height: 20px;
-       list-style-type: disc;
-       margin-bottom: 10px;
+    font-size: 13px;
+    line-height: 20px;
+    list-style-type: disc;
+    margin-bottom: 10px;
+}
+
+.form-row {
+    line-height: 25px;
 }
 
-.form-row { line-height: 25px; }
-table.form-as-table { margin-top: 5px; }
+table.form-as-table {
+    margin-top: 5px;
+}
 
 table.form-as-table ul {
-       display: inline;
-       list-style-type: none;
+    display: inline;
+    list-style-type: none;
+}
+
+table.form-as-table li {
+    display: inline;
+}
+
+table.form-as-table pre {
+    display: inline;
 }
 
-table.form-as-table li { display: inline; }
-table.form-as-table pre { display: inline; }
-table.check-table td { padding-right: 50px; }
+table.check-table td {
+    padding-right: 50px;
+}
 
 .submit-row {
-       clear: both;
-       display: block;
-       line-height: 30px;
-       padding-top: 10px;
+    clear: both;
+    display: block;
+    line-height: 30px;
+    padding-top: 10px;
 }
 
 .error {
-       color: darkred;
-       font-size: 10px;
-       margin: 0;
+    color: darkred;
+    font-size: 10px;
+    margin: 0;
 }
 
 .small {
-       font-size: 11px;
+    font-size: 11px;
 }
 
 span.form-error {
-       color: #990000;
-       font-size: 90%;
-       font-weight: normal;
-       margin-left: 5px;
+    color: #990000;
+    font-size: 90%;
+    font-weight: normal;
+    margin-left: 5px;
 }
 
 .title-desc {
-       color: #666666;
-       font-size: 90%;
+    color: #666666;
+    font-size: 90%;
 }
 
 #editor {
-       font-size: 100%;
-       line-height: 18px;
-       min-height: 200px;
-       width: 100%;
+    font-size: 100%;
+    line-height: 18px;
+    min-height: 200px;
+    width: 100%;
 }
 
 .wmd-preview {
-       background-color: #F5F5F5;
-       margin-top: 10px;
-       min-height: 20px;
-       padding: 6px;
-       width: 98%;
+    background-color: #F5F5F5;
+    margin-top: 10px;
+    min-height: 20px;
+    padding: 6px;
+    width: 98%;
 }
 
 .preview-toggle {
-       color: #AAAAAA;
-       font-weight: 600;
-       text-align: left;
-       width: 100%;
+    color: #AAAAAA;
+    font-weight: 600;
+    text-align: left;
+    width: 100%;
 }
 
-.preview-toggle span:hover { cursor: pointer; }
-#revisions { width: 950px; }
+.preview-toggle span:hover {
+    cursor: pointer;
+}
+
+#revisions {
+    width: 950px;
+}
 
 .revision {
-       font-size: 13px;
-       margin: 10px 0;
-       width: 100%;
+    font-size: 13px;
+    margin: 10px 0;
+    width: 100%;
 }
 
 .revision .header {
-       background-color: #EEEEEE;
-       cursor: pointer;
-       padding: 5px;
+    background-color: #EEEEEE;
+    cursor: pointer;
+    padding: 5px;
+}
+
+.revision .author {
+    background-color: #E9E9FF;
 }
 
-.revision .author { background-color: #E9E9FF; }
-.revision .summary { padding: 5px 0 10px; }
+.revision .summary {
+    padding: 5px 0 10px;
+}
 
 .revision .summary span {
-       background-color: yellow;
-       display: inline;
-       padding-left: 3px;
-       padding-right: 3px;
+    background-color: yellow;
+    display: inline;
+    padding-left: 3px;
+    padding-right: 3px;
 }
 
 .revision h1 {
-       font-size: 130%;
-       font-weight: 600;
-       padding: 15px 0;
+    font-size: 130%;
+    font-weight: 600;
+    padding: 15px 0;
 }
 
 .revision-mark {
-       display: inline-block;
-       font-size: 90%;
-       overflow: hidden;
-       text-align: left;
-       width: 200px;
+    display: inline-block;
+    font-size: 90%;
+    overflow: hidden;
+    text-align: left;
+    width: 200px;
 }
 
 .revision-number {
-       font-family: sans-serif;
-       font-size: 300%;
-       font-weight: bold;
+    font-family: sans-serif;
+    font-size: 300%;
+    font-weight: bold;
 }
 
 .revision .body {
-       margin-bottom: 50px;
-       padding-left: 10px;
+    margin-bottom: 50px;
+    padding-left: 10px;
+}
+
+del {
+    color: #FF5F5F;
 }
 
-del { color: #FF5F5F; }
-ins { background-color: #97FF97; }
+ins {
+    background-color: #97FF97;
+}
 
 .count {
-       color: #777777;
-       font-family: Arial;
-       font-size: 200%;
-       font-weight: 700;
+    color: #777777;
+    font-family: Arial;
+    font-size: 200%;
+    font-weight: 700;
 }
 
 .scoreNumber {
-       color: #777777;
-       font-family: Arial;
-       font-size: 35px;
-       font-weight: 800;
-       line-height: 40px;
+    color: #777777;
+    font-family: Arial;
+    font-size: 35px;
+    font-weight: 800;
+    line-height: 40px;
 }
 
-.user-details { font-size: 13px; }
+.user-details {
+    font-size: 13px;
+}
 
 .user-about {
-       background-color: #EEEEEE;
-       height: 200px;
-       line-height: 20px;
-       overflow: auto;
-       padding: 10px;
-       width: 90%;
+    background-color: #EEEEEE;
+    height: 200px;
+    line-height: 20px;
+    overflow: auto;
+    padding: 10px;
+    width: 90%;
 }
 
 .user-edit-link {
-       background: url("../images/edit.png") no-repeat scroll 0 0 transparent;
-       padding-left: 20px;
+    background: url("../images/edit.png") no-repeat scroll 0 0 transparent;
+    padding-left: 20px;
+}
+
+.user-info-table {
+    margin-bottom: 10px;
 }
 
-.user-info-table { margin-bottom: 10px; }
-.relativetime { text-decoration: none; }
+.relativetime {
+    text-decoration: none;
+}
 
 .answer-summary {
-       clear: both;
-       display: block;
-       padding: 3px;
+    clear: both;
+    display: block;
+    padding: 3px;
 }
 
 .answer-votes {
-       background-color: #EEEEEE;
-       color: #555555;
-       float: left;
-       font-family: Arial;
-       font-size: 110%;
-       font-weight: bold;
-       height: 15px;
-       margin-right: 10px;
-       padding: 4px 4px 5px;
-       text-align: center;
-       text-decoration: none;
-       width: 20px;
+    background-color: #EEEEEE;
+    color: #555555;
+    float: left;
+    font-family: Arial;
+    font-size: 110%;
+    font-weight: bold;
+    height: 15px;
+    margin-right: 10px;
+    padding: 4px 4px 5px;
+    text-align: center;
+    text-decoration: none;
+    width: 20px;
 }
 
 .vote-count {
-       color: #777777;
-       font-family: Arial;
-       font-size: 160%;
-       font-weight: 700;
+    color: #777777;
+    font-family: Arial;
+    font-size: 160%;
+    font-weight: 700;
 }
 
 .user-action-1 {
-       color: #333333;
-       font-weight: bold;
+    color: #333333;
+    font-weight: bold;
 }
 
 .user-action-2 {
-       color: #CCCCCC;
-       font-weight: bold;
+    color: #CCCCCC;
+    font-weight: bold;
 }
 
-.user-action-3 { color: #333333; }
-.user-action-4 { color: #333333; }
-.user-action-7 { color: #333333; }
+.user-action-3 {
+    color: #333333;
+}
+
+.user-action-4 {
+    color: #333333;
+}
+
+.user-action-7 {
+    color: #333333;
+}
 
 .user-action-8 {
-       background-color: #CCCCCC;
-       color: #763333;
-       font-weight: bold;
-       padding: 3px;
+    background-color: #CCCCCC;
+    color: #763333;
+    font-weight: bold;
+    padding: 3px;
 }
 
 .question-title-link a {
-       color: #0077CC;
-       font-weight: bold;
+    color: #0077CC;
+    font-weight: bold;
+}
+
+.answer-title-link a {
+    color: #333333;
+}
+
+.post-type-1 a {
+    font-weight: bold;
+}
+
+.post-type-3 a {
+    font-weight: bold;
 }
 
-.answer-title-link a { color: #333333; }
-.post-type-1 a { font-weight: bold; }
-.post-type-3 a { font-weight: bold; }
-.post-type-2 a { color: #333333; }
-.post-type-4 a { color: #333333; }
-.post-type-8 a { color: #333333; }
-.badge1 { color: #FFCC00; }
-.silver,.badge2 { color: #CCCCCC; }
-.bronze,.badge3 { color: #CC9933; }
+.post-type-2 a {
+    color: #333333;
+}
+
+.post-type-4 a {
+    color: #333333;
+}
+
+.post-type-8 a {
+    color: #333333;
+}
+
+.badge1 {
+    color: #FFCC00;
+}
+
+.silver, .badge2 {
+    color: #CCCCCC;
+}
+
+.bronze, .badge3 {
+    color: #CC9933;
+}
 
 .score {
-       color: #333333;
-       font-size: 110%;
-       font-weight: bold;
-       margin-left: 3px;
+    color: #333333;
+    font-size: 110%;
+    font-weight: bold;
+    margin-left: 3px;
 }
 
 .footerLinks {
-       color: #3060A8;
-       font-size: 13px;
+    color: #3060A8;
+    font-size: 13px;
 }
 
 .footerLinks a {
-       color: #3060A8;
-       font-size: 13px;
+    color: #3060A8;
+    font-size: 13px;
 }
 
 .user {
-       line-height: 140%;
-       padding: 5px;
-       width: 170px;
+    line-height: 140%;
+    padding: 5px;
+    width: 170px;
 }
 
 .user ul {
-       list-style-type: none;
-       margin: 0;
+    list-style-type: none;
+    margin: 0;
 }
 
 .user .thumb {
-       clear: both;
-       display: inline;
-       float: left;
-       margin-right: 4px;
+    clear: both;
+    display: inline;
+    float: left;
+    margin-right: 4px;
 }
 
 .message {
-       background-color: #EEEEEE;
-       border: 1px solid #AAAAAA;
-       margin: 10px 0;
-       padding: 5px;
+    background-color: #EEEEEE;
+    border: 1px solid #AAAAAA;
+    margin: 10px 0;
+    padding: 5px;
+}
+
+.message p {
+    margin-bottom: 0;
 }
 
-.message p { margin-bottom: 0; }
-.darkred { color: darkred; }
+.darkred {
+    color: darkred;
+}
 
 .submit {
-       background-color: #D4D0C8;
-       border: 1px solid #777777;
-       cursor: pointer;
-       font-size: 120%;
-       font-weight: bold;
-       height: 40px;
-       padding-bottom: 4px;
+    background-color: #D4D0C8;
+    border: 1px solid #777777;
+    cursor: pointer;
+    font-size: 120%;
+    font-weight: bold;
+    height: 40px;
+    padding-bottom: 4px;
 }
 
-.submit:hover { text-decoration: underline; }
-.ask-body { padding-right: 10px; }
+.submit:hover {
+    text-decoration: underline;
+}
+
+.ask-body {
+    padding-right: 10px;
+}
 
 .notify {
-       background-color: #F4A83D;
-       color: #444444;
-       font-weight: bold;
-       left: 0;
-       padding: 0;
-       position: fixed;
-       text-align: center;
-       top: 0;
-       width: 100%;
-       z-index: 100;
+    background-color: #F4A83D;
+    color: #444444;
+    font-weight: bold;
+    left: 0;
+    padding: 0;
+    position: fixed;
+    text-align: center;
+    top: 0;
+    width: 100%;
+    z-index: 100;
 }
 
 .notify p {
-       font-size: 16px;
-       margin-bottom: 5px;
-       margin-top: 5px;
+    font-size: 16px;
+    margin-bottom: 5px;
+    margin-top: 5px;
 }
 
 #close-notify {
-       background-color: #FAD163;
-       border: 2px solid #735005;
-       color: #735005;
-       cursor: pointer;
-       font-size: 14px;
-       line-height: 18px;
-       padding: 0 3px;
-       position: absolute;
-       right: 5px;
-       text-decoration: none;
-       top: 5px;
-}
-
-#close-notify:hover { text-decoration: none; }
-.big { font-size: 15px; }
-.strong { font-weight: bold; }
+    background-color: #FAD163;
+    border: 2px solid #735005;
+    color: #735005;
+    cursor: pointer;
+    font-size: 14px;
+    line-height: 18px;
+    padding: 0 3px;
+    position: absolute;
+    right: 5px;
+    text-decoration: none;
+    top: 5px;
+}
+
+#close-notify:hover {
+    text-decoration: none;
+}
+
+.big {
+    font-size: 15px;
+}
+
+.strong {
+    font-weight: bold;
+}
 
 .orange {
-       color: #D64000;
-       font-weight: bold;
+    color: #D64000;
+    font-weight: bold;
 }
 
-.grey { color: #808080; }
+.grey {
+    color: #808080;
+}
 
 .about div {
-       border-top: 1px dashed #AAAAAA;
-       padding: 10px 5px;
+    border-top: 1px dashed #AAAAAA;
+    padding: 10px 5px;
 }
 
 .about div.first {
-       border-top: medium none;
-       padding-top: 0;
+    border-top: medium none;
+    padding-top: 0;
 }
 
-.about p { margin-bottom: 10px; }
+.about p {
+    margin-bottom: 10px;
+}
 
 .about a {
-       color: #D64000;
-       text-decoration: underline;
+    color: #D64000;
+    text-decoration: underline;
 }
 
 .about h3 {
-       font-size: 15px;
-       font-weight: 700;
-       line-height: 30px;
-       padding-top: 0;
+    font-size: 15px;
+    font-weight: 700;
+    line-height: 30px;
+    padding-top: 0;
 }
 
-.nomargin { margin: 0; }
-.inline-block { display: inline-block; }
-.list-table td { vertical-align: top; }
+.nomargin {
+    margin: 0;
+}
+
+.inline-block {
+    display: inline-block;
+}
+
+.list-table td {
+    vertical-align: top;
+}
 
 table.form-as-table input {
-       display: inline;
-       margin-left: 4px;
+    display: inline;
+    margin-left: 4px;
 }
 
 ul.form-horizontal-rows {
-       list-style: none outside none;
-       margin: 0;
+    list-style: none outside none;
+    margin: 0;
 }
 
 ul.form-horizontal-rows li {
-       height: 40px;
-       position: relative;
+    height: 40px;
+    position: relative;
 }
 
-ul.form-horizontal-rows label { display: inline-block; }
+ul.form-horizontal-rows label {
+    display: inline-block;
+}
 
 ul.form-horizontal-rows label {
-       bottom: 6px;
-       font-size: 12px;
-       left: 0;
-       line-height: 12px;
-       margin: 0;
-       position: absolute;
+    bottom: 6px;
+    font-size: 12px;
+    left: 0;
+    line-height: 12px;
+    margin: 0;
+    position: absolute;
 }
 
 ul.form-horizontal-rows li input {
-       bottom: 0;
-       left: 180px;
-       margin: 0;
-       position: absolute;
+    bottom: 0;
+    left: 180px;
+    margin: 0;
+    position: absolute;
 }
 
-#changepw-form li input { left: 150px; }
+#changepw-form li input {
+    left: 150px;
+}
 
 .user-profile-tool-links {
-       font-weight: bold;
-       padding-bottom: 10px;
+    font-weight: bold;
+    padding-bottom: 10px;
 }
 
 .post-controls, .tags-container {
-       font-size: 11px;
-       line-height: 12px;
-       margin-bottom: 5px;
-       min-width: 200px;
+    font-size: 11px;
+    line-height: 12px;
+    margin-bottom: 5px;
+    min-width: 200px;
 }
 
 .tags-container {
-    margin: 0 0 16px 0;    
+    margin: 0 0 16px 0;
 }
 
 .post-controls {
     float: left;
 }
 
-#question-controls .tags { margin: 0 0 3px; }
+#question-controls .tags {
+    margin: 0 0 3px;
+}
 
 .post-update-info {
-       display: inline-block;
-       float: right;
-       margin-bottom: 5px;
-       width: 190px;
+    display: inline-block;
+    float: right;
+    margin-bottom: 5px;
+    width: 190px;
 }
 
 .post-update-info p {
-       font-size: 11px;
-       line-height: 15px;
-       margin: 0 0 4px;
-       padding: 0;
+    font-size: 11px;
+    line-height: 15px;
+    margin: 0 0 4px;
+    padding: 0;
 }
 
 .post-update-info img {
-       float: left;
-       margin: 4px 8px 0 0;
-       width: 32px;
+    float: left;
+    margin: 4px 8px 0 0;
+    width: 32px;
+}
+
+#tagSelector {
+    padding-bottom: 2px;
+}
+
+#hideIgnoredTagsControl {
+    margin: 5px 0 0;
 }
 
-#tagSelector { padding-bottom: 2px; }
-#hideIgnoredTagsControl { margin: 5px 0 0; }
-#hideIgnoredTagsCb { margin: 0 2px 0 1px; }
+#hideIgnoredTagsCb {
+    margin: 0 2px 0 1px;
+}
 
 a.sidebar_button {
-       background: none repeat scroll 0 0 #EEEEEE;
-       color: black;
-       cursor: pointer;
-       font-size: 11px;
-       padding: 3px;
+    background: none repeat scroll 0 0 #EEEEEE;
+    color: black;
+    cursor: pointer;
+    font-size: 11px;
+    padding: 3px;
 }
 
 a.sidebar_button:hover {
-       background-color: #777777;
-       color: white;
-       text-decoration: none;
+    background-color: #777777;
+    color: white;
+    text-decoration: none;
 }
 
-a.post-vote,.favorite-mark,a.accept-answer {
-       display: block;
-       height: 24px;
-       position: relative;
-       width: 24px;
+a.post-vote, .favorite-mark, a.accept-answer {
+    display: block;
+    height: 24px;
+    position: relative;
+    width: 24px;
 }
 
-a.post-vote.up { background: url("../images/vote-arrow-up.png") no-repeat scroll center center transparent; }
-a.post-vote.up.on,a.post-vote.up:hover { background: url("../images/vote-arrow-up-on.png") no-repeat scroll center center transparent; }
-a.post-vote.down { background: url("../images/vote-arrow-down.png") no-repeat scroll center center transparent; }
-a.post-vote.down.on,a.post-vote.down:hover { background: url("../images/vote-arrow-down-on.png") no-repeat scroll center center transparent; }
-a.accept-answer { background: url("../images/vote-accepted.png") no-repeat scroll center center transparent; }
-a.accept-answer.on,a.accept-answer:hover { background: url("../images/vote-accepted-on.png") no-repeat scroll center center transparent; }
+a.post-vote.up {
+    background: url("../images/vote-arrow-up.png") no-repeat scroll center center transparent;
+}
+
+a.post-vote.up.on, a.post-vote.up:hover {
+    background: url("../images/vote-arrow-up-on.png") no-repeat scroll center center transparent;
+}
+
+a.post-vote.down {
+    background: url("../images/vote-arrow-down.png") no-repeat scroll center center transparent;
+}
+
+a.post-vote.down.on, a.post-vote.down:hover {
+    background: url("../images/vote-arrow-down-on.png") no-repeat scroll center center transparent;
+}
+
+a.accept-answer {
+    background: url("../images/vote-accepted.png") no-repeat scroll center center transparent;
+}
+
+a.accept-answer.on, a.accept-answer:hover {
+    background: url("../images/vote-accepted-on.png") no-repeat scroll center center transparent;
+}
 
 .community-wiki {
     font-size: 11px;
@@ -1182,55 +1375,73 @@ a.accept-answer.on,a.accept-answer:hover { background: url("../images/vote-accep
 }
 
 .post-score, .comments-char-left-count {
-       color: #777777;
-       font-family: Arial;
-       font-size: 165%;
-       font-weight: bold;
-       padding: 0 0 3px;
+    color: #777777;
+    font-family: Arial;
+    font-size: 165%;
+    font-weight: bold;
+    padding: 0 0 3px;
+}
+
+.favorite-mark {
+    background: url("../images/vote-favorite-off.png") no-repeat scroll center center transparent;
 }
 
-.favorite-mark { background: url("../images/vote-favorite-off.png") no-repeat scroll center center transparent; }
-.favorite-mark.on,a.favorite-mark:hover { background: url("../images/vote-favorite-on.png") no-repeat scroll center center transparent; }
+.favorite-mark.on, a.favorite-mark:hover {
+    background: url("../images/vote-favorite-on.png") no-repeat scroll center center transparent;
+}
 
 .favorite-count {
-       color: #777777;
-       font-family: Arial;
-       font-size: 100%;
-       font-weight: bold;
-       padding: 0;
+    color: #777777;
+    font-family: Arial;
+    font-size: 100%;
+    font-weight: bold;
+    padding: 0;
 }
 
-.comments-container { clear: both; }
-.comments-container { padding: 0; }
-.answered-by-owner .comments-container { background-color: #E6ECFF; }
-.accepted-answer .comments-container { background-color: #CCFFBF; }
+.comments-container {
+    clear: both;
+}
+
+.comments-container {
+    padding: 0;
+}
+
+.answered-by-owner .comments-container {
+    background-color: #E6ECFF;
+}
+
+.accepted-answer .comments-container {
+    background-color: #CCFFBF;
+}
 
 .comment {
-       border-top: 1px dotted #CCCCCE;
-       margin: 0;
+    border-top: 1px dotted #CCCCCE;
+    margin: 0;
     position: relative;
 }
 
-.comment.not_top_scorer { display: none; }
+.comment.not_top_scorer {
+    display: none;
+}
 
 .comment-score {
-       color: #777777;
-       font-family: Arial;
-       font-size: 16px;
-       font-weight: bold;
-       padding-top: 3px;
-       vertical-align: top;
+    color: #777777;
+    font-family: Arial;
+    font-size: 16px;
+    font-weight: bold;
+    padding-top: 3px;
+    vertical-align: top;
     float: left;
-       width: 22px;
+    width: 22px;
     height: 100%;
     text-align: center;
 }
 
 .comment-text {
-       color: #444444;
-       font-size: 12px;
-       margin: 0 0 0 22px;
-       padding: 0;
+    color: #444444;
+    font-size: 12px;
+    margin: 0 0 0 22px;
+    padding: 0;
 }
 
 .comment-text p {
@@ -1238,8 +1449,8 @@ a.accept-answer.on,a.accept-answer:hover { background: url("../images/vote-accep
 }
 
 .comment-info {
-       font-size: 11px;
-       margin: 0 0 4px 0;
+    font-size: 11px;
+    margin: 0 0 4px 0;
     text-align: right;
     height: 18px;
     vertical-align: middle;
@@ -1247,69 +1458,88 @@ a.accept-answer.on,a.accept-answer:hover { background: url("../images/vote-accep
 
 .comment-info * {
     float: right;
-       height: 18px;
-       margin-left: 4px;
+    height: 18px;
+    margin-left: 4px;
+}
+
+a.comment-like, a.comment-delete, a.comment-edit {
+    margin-left: 2px;
+    width: 18px;
 }
 
-a.comment-like,a.comment-delete,a.comment-edit {
-       margin-left: 2px;
-       width: 18px;
+a.comment-like {
+    background: url("../images/comment-like.png") no-repeat scroll center center transparent;
 }
 
-a.comment-like { background: url("../images/comment-like.png") no-repeat scroll center center transparent; }
-a.comment-like:hover,a.comment-like.on { background: url("../images/comment-like-on.png") no-repeat scroll center center transparent; }
-a.comment-delete { background: url("../images/comment-delete.png") no-repeat scroll center center transparent; }
-a.comment-delete:hover { background: url("../images/comment-delete-hover.png") no-repeat scroll center center transparent; }
-a.comment-edit { background: url("../images/comment-edit.png") no-repeat scroll center center transparent; }
-a.comment-edit:hover { background: url("../images/comment-edit-hover.png") no-repeat scroll center center transparent; }
+a.comment-like:hover, a.comment-like.on {
+    background: url("../images/comment-like-on.png") no-repeat scroll center center transparent;
+}
+
+a.comment-delete {
+    background: url("../images/comment-delete.png") no-repeat scroll center center transparent;
+}
+
+a.comment-delete:hover {
+    background: url("../images/comment-delete-hover.png") no-repeat scroll center center transparent;
+}
+
+a.comment-edit {
+    background: url("../images/comment-edit.png") no-repeat scroll center center transparent;
+}
+
+a.comment-edit:hover {
+    background: url("../images/comment-edit-hover.png") no-repeat scroll center center transparent;
+}
 
 .comment-form-container {
-       display: none;
-       padding-top: 12px;
+    display: none;
+    padding-top: 12px;
 }
 
-.comment-form-widgets-container input { vertical-align: top; }
+.comment-form-widgets-container input {
+    vertical-align: top;
+}
 
 .comment-form-widgets-container textarea {
-       height: 80px;
-       width: 80%;
+    height: 80px;
+    width: 80%;
     float: left;
 }
 
 span.comment-chars-left {
-       font-size: 11px;
-       margin-right: 20px;
+    font-size: 11px;
+    margin-right: 20px;
 }
 
 div.comment-tools {
-       border-top: 1px dotted #CCCCCE;
-       padding-top: 12px;
-       text-align: right;
+    border-top: 1px dotted #CCCCCE;
+    padding-top: 12px;
+    text-align: right;
 }
 
 div.comment-tools .comments-showing {
-       color: #777777;
-       font-size: 11px;
+    color: #777777;
+    font-size: 11px;
 }
 
 div.comment-tools a {
-       background: none repeat scroll 0 0 #EEEEEE;
-       color: black;
-       cursor: pointer;
-       font-size: 11px;
-       padding: 3px;
+    background: none repeat scroll 0 0 #EEEEEE;
+    color: black;
+    cursor: pointer;
+    font-size: 11px;
+    padding: 3px;
 }
 
 div.comment-tools a:hover {
-       background-color: #777777;
-       color: white;
-       text-decoration: none;
+    background-color: #777777;
+    color: white;
+    text-decoration: none;
 }
 
 .action-link {
-       color: #777777;
-       cursor: pointer;
-       padding: 3px;
+    color: #777777;
+    cursor: pointer;
+    padding: 3px;
 }
 
 .action-link a {
@@ -1317,14 +1547,18 @@ div.comment-tools a:hover {
 }
 
 .action-link a.ajax-command:hover {
-       background-color: #777777;
-       color: #FFFFFF;
-       text-decoration: none;
+    background-color: #777777;
+    color: #FFFFFF;
+    text-decoration: none;
 }
 
-.action-link-separator { color: #CCCCCC; }
+.action-link-separator {
+    color: #CCCCCC;
+}
 
-.deleted {background-color: #F4E7E7;}
+.deleted {
+    background-color: #F4E7E7;
+}
 
 #command-loader {
     position: fixed;
@@ -1360,7 +1594,7 @@ div.comment-tools a:hover {
 }
 
 .comments-char-left-count.warn {
-    color: orange;    
+    color: orange;
 }
 
 #ask-related-questions {
@@ -1391,7 +1625,7 @@ div.dialog, .context-menu-dropdown {
 
 .context-menu-dropdown li.item {
     padding: 4px 8px 4px 8px;
-     -moz-border-radius: 5px;
+    -moz-border-radius: 5px;
     -webkit-border-radius: 5px;
 }
 
@@ -1471,4 +1705,8 @@ div.dialog.prompt .dialog-content select, div.dialog.prompt .dialog-content text
 
 .user-prompt .prompt-buttons {
     text-align: right;
+}
+
+.suspended-user {
+    text-decoration: line-through;
 }
\ No newline at end of file
diff --git a/forum/skins/default/templates/403.html b/forum/skins/default/templates/403.html
new file mode 100644 (file)
index 0000000..583e550
--- /dev/null
@@ -0,0 +1,47 @@
+{% extends "base_content.html" %}
+{% load i18n %}
+{% block title %}{% trans "Forbidden" %}{% endblock %}
+{% block forestyle%}
+    <style type="text/css">
+               form input { margin-right: 5px; }
+       </style>
+{% endblock %}
+{% block forejs %}
+       <script type="text/javascript">
+        $().ready(function(){
+            $("#linkPrevious").bind("click", back=function(){history.go(-1);})
+        });
+
+        </script>
+{% endblock %}
+{% block content %}
+<div id="main-bar" class="headNormal">
+    {% trans "Forbidden" %}
+</div>
+<div id="main-body" class="">
+    <div style="padding:5px 0px 10px 0;line-height:25px;">
+        <h3>{% trans "Sorry, could not find the page you requested." %}</h3>
+        <div style="margin-top:5px">
+        {% trans "This might have happened for the following reasons:" %}<br/>
+            <ul>
+            <li>{% trans "this question or answer has been deleted;" %}</li>
+            <li>{% trans "url has error - please check it;" %}</li>
+            <li>{% trans "the page you tried to visit is protected or you don't have sufficient points, see" %} <a href="{% url faq %}"> faq</a>;</li>
+            <li>{% trans "if you believe this error 404 should not have occured, please" %}
+                               <a href="{{feedback_site_url}}" target="_blank">{% trans "report this problem" %}</a></li>
+            </ul>
+        </div>
+        <script type="text/javascript">
+            var GOOG_FIXURL_LANG = '{{settings.LANGUAGE_CODE}}';
+            var GOOG_FIXURL_SITE = '{{site_url}}';
+        </script>
+        <script type="text/javascript" src="http://linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script>
+        <ul>
+            <li><a href="#" id="linkPrevious">{% trans "back to previous page" %} Ãˆ</a></li>
+            <li><a href="{% url questions %}">{% trans "see all questions" %} Ãˆ</a></li>
+            <li><a href="{% url tags %}">{% trans "see all tags" %} Ãˆ</a></li>
+        </ul>
+    </div>
+
+</div>
+{% endblock %}
index d02a42144fc756e48f57f1da2d5d42583eaf24d8..067e5ab2470084d2d6febfd3b33b2f0e48cc45ac 100644 (file)
@@ -1,7 +1,7 @@
 {% extends "base_content.html" %}
 <!-- template 404.html -->
 {% load i18n %}
-{% block title %}{% spaceless %}404 Error{% endspaceless %}{% endblock %}
+{% block title %}{% trans "404 Error" %}{% endblock %}
 {% block forestyle%}
     <style type="text/css">
                form input { margin-right: 5px; }
@@ -17,7 +17,7 @@
 {% endblock %}
 {% block content %}
 <div id="main-bar" class="headNormal">
-    404 Not Found
+    {% trans "404 Not Found" %}
 </div>
 <div id="main-body" class="">
     <div style="padding:5px 0px 10px 0;line-height:25px;">
index 43b1553b55cb6805dd3d0fa1841c3c9fd3aa4df3..71b454a14b41da52b8cfc4ceb37292c259aff8f3 100644 (file)
@@ -39,8 +39,8 @@
         <a href="#" class="add-comment-link">{% trans "add new comment" %}</a>\r
     {% endif %}\r
 </div>\r
-{% if can_comment %}\r
 <div id="comment-{{ post.id }}-form-container" class="comment-form-container">\r
+    {% if can_comment %}\r
     <form id="comment-{{ post.id }}-form" method="post" action="{% url comment id=post.id %}" accept-charset="utf-8">\r
         <div class="comment-form-widgets-container">\r
             <textarea name="comment"></textarea>\r
@@ -72,6 +72,6 @@
              </div>\r
         </script>\r
     </form>\r
+    {% endif %}\r
 </div>\r
-{% endif %}\r
     
\ No newline at end of file
index 54e0bbe2f0e8b6d5403f3164e78a7bfb5087dfdc..0c40cf2762d369a9d460d8ef9bf80f1f8cdcc4ba 100644 (file)
@@ -5,7 +5,7 @@
         <strong>{% diff_date node.added_at %}</strong>
     </p>
     {% gravatar node.author 32 %}
-    <p><a href="{{ node.author.get_profile_url }}">{{ node.author.username }}</a><br/>
+    <p><a {% if node.author.is_suspended %}class="suspended-user" {% endif %}href="{{ node.author.get_profile_url }}">{{ node.author.username }}</a><br/>
     {% get_score_badge node.author %}</p>
 </div>
 {% if node.last_edited %}
@@ -18,7 +18,7 @@
         </p>
         {% ifnotequal node.author node.last_edited.by %}
             {% gravatar node.last_edited.by 32 %}
-            <p><a href="{{ node.last_edited.by.get_profile_url }}">{{ node.last_edited.by.username }}</a><br/>
+            <p><a {% if node.last_edited.by.is_suspended %}class="suspended-user" {% endif %}href="{{ node.last_edited.by.get_profile_url }}">{{ node.last_edited.by.username }}</a><br/>
             {% get_score_badge node.last_edited.by %}</p>
         {% endifnotequal %}
     </div>
index dcea80fa3550e52464dc91f7731648f1c63fcf90..0cf3ee115af00ebf306f6e6b1ac81c5fdd41bf4f 100644 (file)
                     <ul>
                         <li><a href="{% url admin_maintenance %}">{% trans "Maintenance mode" %}</a></li>
                         <li><a href="{% url admin_flagged_posts %}">{% trans "Flagged Posts" %}</a></li>
+                        {% comment %}<li><a href="{% url admin_moderation %}">{% trans "Moderation" %}</a></li>{% endcomment %}
                     </ul>
                 </div>
             </div>
diff --git a/forum/skins/default/templates/osqaadmin/moderation.html b/forum/skins/default/templates/osqaadmin/moderation.html
new file mode 100644 (file)
index 0000000..688c897
--- /dev/null
@@ -0,0 +1,101 @@
+{% extends basetemplate %}
+
+{% load i18n humanize %}
+
+{% block subtitle %}{% trans "Moderation" %}{% endblock %}
+{% block pagename %}{% trans "Moderation" %}{% endblock %}
+{% block description %}{% trans "These tools allow you to search for undesired behaviours and cheating patterns." %}{% endblock %}
+
+{% block admincontent %}
+    <div class="module">
+        <form action="" id="changelist" method="POST">
+            <div class="actions">
+                {% trans "Verify:" %}
+                <input type="text" size="3" name="limit" id="filter-limit" value="5" />
+                <select name="sort" id="filter-sort">
+                    <option value="high-rep">{% trans "highest ranking users" %}</option>
+                    <option value="newer">{% trans "newer users" %}</option>
+                    <option value="older">{% trans "older users" %}</option>
+                    <option value="ids">{% trans "users with these ids" %}</option>
+                </select>
+                <span id="filter-ids" style="display: none">
+                    <input type="text" name="ids" size="15" />
+                    <small>{% trans "(Comma separated list of user ids)" %}</small>
+                </span>
+                <input type="submit" value="{% trans "Go" %}" />
+            </div>
+        </form>
+        <script type="text/javascript">
+            $(function() {
+                $limit = $('#filter-limit');
+                $sort = $('#filter-sort');
+                $ids = $('#filter-ids');
+
+                function verify_sort() {
+                    if ($sort.val() == "ids") {
+                        $ids.show();
+                        $limit.hide();
+                    } else {
+                        $ids.hide();
+                        $limit.show();
+                    }
+                }
+
+                verify_sort();
+                $sort.change(verify_sort);
+            })
+        </script>
+        {% if cheaters %}
+        <table cellspacing="0" width="100%">
+            <caption>{% trans "Possible cheaters" %}</caption>
+            {% for cheater, fakes in cheaters %}
+            <tr>
+                <td>
+                    <div class="cheater-info">
+                        <p><a href="{{ cheater.get_profile_url }}">{{ cheater.username }}</a></p>
+                        <p><b>{% trans "Email" %}</b>
+                        {% if cheater.email_isvalid %}
+                            <img src="{{ settings.ADMIN_MEDIA_PREFIX }}img/admin/icon-yes.gif" alt="{% trans "Validated" %}" />
+                        {% else %}
+                            <img src="{{ settings.ADMIN_MEDIA_PREFIX }}img/admin/icon-no.gif" alt="{% trans "Not validated" %}" />
+                        {% endif %}
+                        <a href="mailto: {{ cheater.email }}">{{ cheater.email }}</a></p>
+                        <p><b>{% trans "Reputation:" %}</b> {{ cheater.reputation|intcomma }}</p>
+                    </div>
+                    <table cellspacing="0" width="100%">
+                        <thead>
+                            <tr>
+                                <th>{% trans "Profile" %}</th>
+                                <th>{% trans "Email" %}</th>
+                                <th>{% trans "Reputation" %}</th>
+                                <th>{% trans "Affecting actions" %}</th>
+                                <th>{% trans "Cross ips" %}</th>
+                                <th>{% trans "Cheating score" %}</th>
+                            </tr>
+                        </thead>
+                        <caption>{% trans "Possible fake accounts" %}</caption>
+                        {% for fake in fakes %}
+                            <tr>
+                                <td><a href="{{ fake.get_profile_url }}">{{ fake.username }}</a></td>
+                                <td>
+                                    {% if fake.email_isvalid %}
+                                        <img src="{{ settings.ADMIN_MEDIA_PREFIX }}img/admin/icon-yes.gif" alt="{% trans "Validated" %}" />
+                                    {% else %}
+                                        <img src="{{ settings.ADMIN_MEDIA_PREFIX }}img/admin/icon-no.gif" alt="{% trans "Not validated" %}" />
+                                    {% endif %}
+                                    <a href="mailto: {{ fake.email }}">{{ fake.email }}</a>
+                                </td>
+                                <td>{{ fake.reputation|intcomma }}</td>
+                                <td>{{ fake.fdata.affect_count }} {% trans "out of" %} {{ fake.fdata.total_actions }} ({{ fake.fdata.action_ratio|stringformat:".2f" }}%)</td>
+                                <td>{{ fake.fdata.cross_ip_count }} {% trans "out of" %} {{ fake.fdata.total_ip_count }} ({{ fake.fdata.cross_ip_ratio|stringformat:".2f" }}%)</td>
+                                <td>{{ fake.fdata.fake_score|stringformat:".2f" }}</td>
+                            </tr>
+                        {% endfor %}
+                    </table>
+                </td>
+            </tr>
+            {% endfor %}
+        </table>
+        {% endif %}
+    </div>
+{% endblock %}
\ No newline at end of file
index 731ec3a0d6620a0fbf45a419bee138f69ee611ad..65d726e635f94218e2197eff4f161d10ca7e52fc 100644 (file)
@@ -1,6 +1,10 @@
 {% load i18n %}\r
 {% load smart_if %}\r
 \r
+<script type="text/javascript">\r
+    messages['points'] = "{% trans "Points" %}"\r
+</script>\r
+\r
 <div id="user-menu-container">\r
     <span id="user-menu">{% trans "User tools" %} &#9660;</span>\r
     <ul id="user-menu-dropdown">\r
         {% ifnotequal user viewer %}\r
             {% if viewer.is_superuser %}\r
             <li class="separator">{% trans "Moderation tools" %}</li>\r
+            {% if not user.is_superuser %}\r
+                {% if user.is_suspended %}\r
+                <li class="item"><span></span>\r
+                    <a href="{% url user_suspend id=user.id %}" class="ajax-command confirm" id="suspend-user">{% trans "withdraw suspension" %}</a>\r
+                </li>\r
+                {% else %}\r
+                <li class="item"><span></span>\r
+                    <a href="{% url user_suspend id=user.id %}" class="ajax-command withprompt" id="suspend-user">{% trans "suspend this user" %}</a>\r
+                </li>\r
+                {% endif %}\r
+            {% endif %}\r
+\r
             <li class="item"><span class="user-award_rep"></span><a href="{% url user_award_points id=user.id %}" id="award-rep-points">{% trans "reputation bonus" %}</a></li>\r
                 {% if not user.is_superuser %}\r
                     {% if not user.is_staff %}\r
     </ul>\r
 </div>\r
 \r
-{% comment %}\r
-<h3>{% trans "Moderation tools" %}</h3>\r
-<p><a href="#" id="point-award-action">{% trans "Reputation bonus" %}</a></p>\r
-<form action="" method="POST">\r
-    <table style="display: none" id="award-points-table" class="moderation-table">\r
-    {{ awardform.as_table }}\r
-    <tr><td colspan="2" class="moderation-table-footer"><input type="submit" id="award-points-submit" value="{% trans "Send" %}" /></td></tr>\r
-    </table>    \r
-</form>\r
-<script>\r
-    $(function() {\r
-        $('#point-award-action').click(function() {\r
-            $('#award-points-table').slideToggle('slow');\r
-        });\r
-\r
-        $('#award-points-submit').click(function() {\r
-            $('#award-points-table').find('.error').remove();\r
-            var $points_input = $('#award-points-table').find('input[type=text]');\r
-            var points = parseInt($points_input.val());\r
-\r
-            if (isNaN(points) || points < 1) {\r
-                $points_input.before('<p class="error">{% trans "Sorry but that\'s not a valid input" %}</p>');\r
-                return false;\r
-            }\r
-\r
-            $.post('{% url user_award_points id=user.id %}')\r
-        });\r
-    });\r
-</script>\r
-{% if not user.is_superuser %}\r
-<p><a href="{% url user_powers id=user.id,action="grant",status="super" %}">{% trans "Grant super user status" %}</a></p>\r
-    {% if not user.is_staff %}\r
-    <p><a href="{% url user_powers id=user.id,action="grant",status="staff" %}">{% trans "Grant moderator status" %}</a></p>\r
-    {% else %}\r
-    <p><a href="{% url user_powers id=user.id,action="remove",status="staff" %}" class="">{% trans "Remove moderator status" %}</a></p>\r
-    {% endif %}\r
-{% else %}\r
-    {% ifequal moderator.id 1 %}\r
-        {% ifnotequal user.id 1 %}\r
-            <p><a href="{% url user_powers id=user.id,action="remove",status="super" %}">{% trans "Remove super user status" %}</a></p>\r
-        {% endifnotequal %}\r
-    {% endifequal %}\r
-{% endif %}\r
-\r
-{% endcomment %}\r
index a34893ec9dbc5cf2ed7404f3717941d00a5d47b6..2bf9aadc5ca84cbaae5f83c5099cd171d1096884 100644 (file)
@@ -1,28 +1,33 @@
 {% load i18n %}{% spaceless %}\r
-{% ifequal format "full" %}\r
 \r
-{% else %}\r
-    <a href="{{ user.get_absolute_url }}">{{ user.username }}</a>\r
-    <span class="score" title="{{ user.reputation }} {% trans "reputation" %}">{{ user.reputation }}</span>\r
-    {% ifequal format "badges" %}\r
-        {% if user.gold %}\r
-        <span title="{{ user.gold }} {% trans "badges" %}">\r
-            <span class="badge1">&#9679;</span>\r
-            <span class="badgecount">{{ user.gold }}</span>\r
-        </span>\r
-        {% endif %}\r
-        {% if user.silver %}\r
-        <span title="{{ user.silver }} {% trans "badges" %}">\r
-            <span class="silver">&#9679;</span>\r
-            <span class="badgecount">{{ user.silver }}</span>\r
-        </span>\r
-        {% endif %}\r
-        {% if user.bronze %}\r
-        <span title="{{ user.bronze }} {% trans "badges" %}">\r
-            <span class="bronze">&#9679;</span>\r
-            <span class="badgecount">{{ user.bronze }}</span>\r
-        </span>\r
-        {% endif %}\r
+{% if not user.is_suspended %}\r
+    {% ifequal format "full" %}\r
+\r
+    {% else %}\r
+        <a href="{{ user.get_absolute_url }}">{{ user.username }}</a>\r
+        <span class="score" title="{{ user.reputation }} {% trans "reputation" %}">{{ user.reputation }}</span>\r
+        {% ifequal format "badges" %}\r
+            {% if user.gold %}\r
+            <span title="{{ user.gold }} {% trans "badges" %}">\r
+                <span class="badge1">&#9679;</span>\r
+                <span class="badgecount">{{ user.gold }}</span>\r
+            </span>\r
+            {% endif %}\r
+            {% if user.silver %}\r
+            <span title="{{ user.silver }} {% trans "badges" %}">\r
+                <span class="silver">&#9679;</span>\r
+                <span class="badgecount">{{ user.silver }}</span>\r
+            </span>\r
+            {% endif %}\r
+            {% if user.bronze %}\r
+            <span title="{{ user.bronze }} {% trans "badges" %}">\r
+                <span class="bronze">&#9679;</span>\r
+                <span class="badgecount">{{ user.bronze }}</span>\r
+            </span>\r
+            {% endif %}\r
+        {% endifequal %}\r
     {% endifequal %}\r
-{% endifequal %}\r
-{% endspaceless %}
\ No newline at end of file
+{% else %}\r
+    <a class="suspended-user" href="{{ user.get_absolute_url }}">{{ user.username }}</a>{% trans "(suspended)" %}\r
+{% endif %}\r
+{% endspaceless %}\r
diff --git a/forum/skins/default/templates/users/suspend_user.html b/forum/skins/default/templates/users/suspend_user.html
new file mode 100644 (file)
index 0000000..e131418
--- /dev/null
@@ -0,0 +1,49 @@
+{% load i18n %}
+
+<table>
+    <caption><h2>{% trans "Suspend user" %}</h2></caption>
+    <tr>
+        <td>
+            <select name="bantype" id="bantype">
+                <option value="indefinitely">{% trans "Indefinetly" %}</option>
+                <option value="forxdays">{% trans "For X days" %}</option>
+            </select>
+        </td>
+    </tr>
+    <tr id="forxdays" style="display: none">
+        <td>
+            {% trans "Suspend for" %}<input type="text" size="3" style="width: 30px; height: 1.2em; margin: 0 0.5em 0 0.5em; font-size: 1em;" name="forxdays" value="3" />{% trans "days" %}
+        </td>
+    </tr>
+    <tr>
+        <td>
+            <p><b>{% trans "Public message" %}:</b></p>
+        </td>
+    </tr>
+    <tr>
+        <td>
+            <textarea rows="3" name="publicmsg" cols="35"></textarea><br />
+            <small>{% trans "This message will be visible through the user activity log."  %}</small>
+        </td>
+    </tr>
+    <tr>
+        <td>
+            <p><b>{% trans "Private message" %}:</b></p>
+        </td>
+    </tr>
+    <tr>
+        <td>
+            <textarea rows="3" name="privatemsg" cols="35"></textarea><br />
+            <small>{% trans "If set, only the suspended user will see this message."  %}</small>
+        </td>
+    </tr>
+</table>
+<script type="text/javascript">
+    $('#bantype').change(function() {
+        if ($(this).val() == 'forxdays') {
+            $('#forxdays').slideDown('fast');
+        } else {
+            $('#forxdays').slideUp('fast');
+        }
+    });
+</script>
\ No newline at end of file
index cc4daebe12c7855d059ee111a96801b51a17b769..2247f5b539d9ead4f1b6690ef57d33eea9529526 100644 (file)
                     
                     <div class="user">
                         <ul>
-                                <li class="thumb"><a href=" {{ user.get_profile_url }} ">{% gravatar user 32 %}</a></li>
-                                <li><a href=" {{ user.get_profile_url }} ">{{ user.get_profile_link }}</a></li> 
-                                <li>{% get_score_badge user %}</li>
-                            </ul>
+                            <li class="thumb"><a href="{{ user.get_profile_url }} ">{% gravatar user 32 %}</a></li>
+                            <li><a {% if user.is_suspended %}class="suspended-user" {% endif %}href=" {{ user.get_profile_url }} ">{{ user.get_profile_link }}</a></li>
+                            <li>{% get_score_badge user %}</li>
+                        </ul>
                     </div>
                     
             {% if forloop.counter|divisibleby:"7" %}
index 6c3da35e30e0ba2e3b46b007889e609b0954ac36..92cb4db2375557e2dfd195ac1b323935263acbe2 100644 (file)
@@ -106,6 +106,9 @@ def cnprog_pagesize(context):
 
 @register.simple_tag
 def get_score_badge(user):
+    if user.is_suspended():
+        return _("(suspended)")
+
     BADGE_TEMPLATE = '<span class="score" title="%(reputation)s %(reputationword)s">%(reputation)s</span>'
     if user.gold > 0 :
         BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, '<span title="%(gold)s %(badgesword)s">'
index 5c784d340b1d2039d288af14cb24025fb97233e8..0c36ffe5283af95bb8127ce6fbafc3dcd21570dd 100644 (file)
@@ -13,10 +13,10 @@ import logging
 
 admin.autodiscover()
 feeds = {
-    'rss': RssLastestQuestionsFeed
+'rss': RssLastestQuestionsFeed
 }
 sitemaps = {
-    'questions': QuestionsSitemap
+'questions': QuestionsSitemap
 }
 
 APP_PATH = os.path.dirname(__file__)
@@ -33,122 +33,168 @@ for pattern_file in module_patterns:
         urlpatterns += pattern
 
 urlpatterns += patterns('',
-    url(r'^$', app.readers.index, name='index'),
-    url(r'^sitemap.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps}, name='sitemap'),
-
-    (r'^favicon\.ico$', app.meta.favicon),
-    url(r'^cstyle\.css$', app.meta.custom_css, name="custom_css"),
-
-    url(r'^m/(?P<skin>\w+)/media/(?P<path>.*)$', app.meta.media , name='osqa_media'),
-    url(r'^%s(?P<path>.*)$' % _('upfiles/'), 'django.views.static.serve',
-        {'document_root': os.path.join(APP_PATH,'upfiles').replace('\\','/')},
-        name='uploaded_file',
-    ),
-
-    url(r'^%s$' % _('faq/'), app.meta.static, {'content': settings.FAQ_PAGE_TEXT, 'title': _('FAQ')}, name='faq'),
-    url(r'^%s$' % _('about/'), app.meta.static, {'content': settings.ABOUT_PAGE_TEXT, 'title': _('About')}, name='about'),
-    url(r'^%s$' % _('markdown_help/'), app.meta.markdown_help, name='markdown_help'),
-    url(r'^opensearch\.xml$', app.meta.opensearch, name='opensearch'),
-    url(r'^%s$' % _('privacy/'), app.meta.privacy, name='privacy'),
-    url(r'^%s$' % _('logout/'), app.meta.logout, name='logout'),
-    url(r'^%s(?P<id>\d+)/%s$' % (_('answers/'), _('edit/')), app.writers.edit_answer, name='edit_answer'),
-    url(r'^%s(?P<id>\d+)/$' % _('revisions/'), app.readers.revisions, name='revisions'),
-    url(r'^%s$' % _('questions/'), app.readers.questions, name='questions'),
-    url(r'^%s%s$' % (_('questions/'), _('ask/')), app.writers.ask, name='ask'),
-    url(r'^%s%s$' % (_('questions/'), _('related_questions/')), app.commands.related_questions, name='related_questions'),
-
-    url(r'^%s%s$' % (_('questions/'), _('unanswered/')), app.readers.unanswered, name='unanswered'),
-    url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('edit/')), app.writers.edit_question, name='edit_question'),
-    url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('close/')), app.commands.close, kwargs=dict(close=True), name='close'),
-    url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('reopen/')), app.commands.close, kwargs=dict(close=False), name='reopen'),
-    url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('answer/')), app.writers.answer, name='answer'),
-
-    url(r'^%s(?P<id>\d+)/(?P<vote_type>[a-z]+)/' % _('vote/'), app.commands.vote_post, name='vote_post'),
-    url(r'^%s(?P<id>\d+)/$' % _('like_comment/'), app.commands.like_comment, name="like_comment"),
-    url(r'^%s(?P<id>\d+)/' % _('comment/'), app.commands.comment, name='comment'),
-    url(r'^%s(?P<id>\d+)/$' % _('delete_comment/'), app.commands.delete_comment, name="delete_comment"),
-    url(r'^%s(?P<id>\d+)/$' % _('accept_answer/'), app.commands.accept_answer, name="accept_answer"),
-    url(r'^%s(?P<id>\d+)/$' % _('mark_favorite/'), app.commands.mark_favorite, name="mark_favorite"),
-    url(r'^%s(?P<id>\d+)/' % _('flag/'), app.commands.flag_post, name='flag_post'),
-    url(r'^%s(?P<id>\d+)/' % _('delete/'), app.commands.delete_post, name='delete_post'),
-    url(r'^%s(?P<id>\d+)/$' % _('subscribe/'), app.commands.subscribe, name="subscribe"),
-    url(r'^%s' % _('matching_tags/'), app.commands.matching_tags, name='matching_tags'),
-    url(r'^%s(?P<id>\d+)/' % _('node_markdown/'), app.commands.node_markdown, name='node_markdown'),
-    url(r'^%s(?P<id>\d+)/' % _('convert/'), app.commands.convert_to_comment, name='convert_to_comment'),
-    url(r'^%s(?P<id>\d+)/' % _('wikify/'), app.commands.wikify, name='wikify'),
-
-    url(r'^%s(?P<id>\d+)/(?P<slug>[\w-]*)$' % _('question/'), 'django.views.generic.simple.redirect_to', {'url': '/questions/%(id)s/%(slug)s'}),
-    url(r'^%s(?P<id>\d+)/(?P<slug>[\w-]*)$' % _('questions/'), app.readers.question, name='question'),
-    url(r'^%s$' % _('tags/'), app.readers.tags, name='tags'),
-    url(r'^%s(?P<tag>.*)/$' % _('tags/'), app.readers.tag, name='tag_questions'),
-
-    url(r'^%s%s(?P<tag>[^/]+)/$' % (_('mark-tag/'),_('interesting/')), app.commands.mark_tag, \
+                        url(r'^$', app.readers.index, name='index'),
+                        url(r'^sitemap.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps},
+                            name='sitemap'),
+
+                        (r'^favicon\.ico$', app.meta.favicon),
+                        url(r'^cstyle\.css$', app.meta.custom_css, name="custom_css"),
+
+                        url(r'^m/(?P<skin>\w+)/media/(?P<path>.*)$', app.meta.media , name='osqa_media'),
+                        url(r'^%s(?P<path>.*)$' % _('upfiles/'), 'django.views.static.serve',
+                            {'document_root': os.path.join(APP_PATH, 'upfiles').replace('\\', '/')},
+                            name='uploaded_file',
+                            ),
+
+                        url(r'^%s$' % _('faq/'), app.meta.static, {'content': settings.FAQ_PAGE_TEXT, 'title': _('FAQ')}
+                            , name='faq'),
+                        url(r'^%s$' % _('about/'), app.meta.static,
+                            {'content': settings.ABOUT_PAGE_TEXT, 'title': _('About')}, name='about'),
+                        url(r'^%s$' % _('markdown_help/'), app.meta.markdown_help, name='markdown_help'),
+                        url(r'^opensearch\.xml$', app.meta.opensearch, name='opensearch'),
+                        url(r'^%s$' % _('privacy/'), app.meta.privacy, name='privacy'),
+                        url(r'^%s$' % _('logout/'), app.meta.logout, name='logout'),
+                        url(r'^%s(?P<id>\d+)/%s$' % (_('answers/'), _('edit/')), app.writers.edit_answer,
+                            name='edit_answer'),
+                        url(r'^%s(?P<id>\d+)/$' % _('revisions/'), app.readers.revisions, name='revisions'),
+                        url(r'^%s$' % _('questions/'), app.readers.questions, name='questions'),
+                        url(r'^%s%s$' % (_('questions/'), _('ask/')), app.writers.ask, name='ask'),
+                        url(r'^%s%s$' % (_('questions/'), _('related_questions/')), app.commands.related_questions,
+                            name='related_questions'),
+
+                        url(r'^%s%s$' % (_('questions/'), _('unanswered/')), app.readers.unanswered, name='unanswered'),
+                        url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('edit/')), app.writers.edit_question,
+                            name='edit_question'),
+                        url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('close/')), app.commands.close,
+                            kwargs=dict(close=True), name='close'),
+                        url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('reopen/')), app.commands.close,
+                            kwargs=dict(close=False), name='reopen'),
+                        url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('answer/')), app.writers.answer, name='answer'),
+
+                        url(r'^%s(?P<id>\d+)/(?P<vote_type>[a-z]+)/' % _('vote/'), app.commands.vote_post,
+                            name='vote_post'),
+                        url(r'^%s(?P<id>\d+)/$' % _('like_comment/'), app.commands.like_comment, name="like_comment"),
+                        url(r'^%s(?P<id>\d+)/' % _('comment/'), app.commands.comment, name='comment'),
+                        url(r'^%s(?P<id>\d+)/$' % _('delete_comment/'), app.commands.delete_comment,
+                            name="delete_comment"),
+                        url(r'^%s(?P<id>\d+)/$' % _('accept_answer/'), app.commands.accept_answer, name="accept_answer")
+                        ,
+                        url(r'^%s(?P<id>\d+)/$' % _('mark_favorite/'), app.commands.mark_favorite, name="mark_favorite")
+                        ,
+                        url(r'^%s(?P<id>\d+)/' % _('flag/'), app.commands.flag_post, name='flag_post'),
+                        url(r'^%s(?P<id>\d+)/' % _('delete/'), app.commands.delete_post, name='delete_post'),
+                        url(r'^%s(?P<id>\d+)/$' % _('subscribe/'), app.commands.subscribe, name="subscribe"),
+                        url(r'^%s' % _('matching_tags/'), app.commands.matching_tags, name='matching_tags'),
+                        url(r'^%s(?P<id>\d+)/' % _('node_markdown/'), app.commands.node_markdown, name='node_markdown'),
+                        url(r'^%s(?P<id>\d+)/' % _('convert/'), app.commands.convert_to_comment,
+                            name='convert_to_comment'),
+                        url(r'^%s(?P<id>\d+)/' % _('wikify/'), app.commands.wikify, name='wikify'),
+
+                        url(r'^%s(?P<id>\d+)/(?P<slug>[\w-]*)$' % _('question/'),
+                            'django.views.generic.simple.redirect_to', {'url': '/questions/%(id)s/%(slug)s'}),
+                        url(r'^%s(?P<id>\d+)/(?P<slug>[\w-]*)$' % _('questions/'), app.readers.question, name='question'
+                            ),
+                        url(r'^%s$' % _('tags/'), app.readers.tags, name='tags'),
+                        url(r'^%s(?P<tag>.*)/$' % _('tags/'), app.readers.tag, name='tag_questions'),
+
+                        url(r'^%s%s(?P<tag>[^/]+)/$' % (_('mark-tag/'),_('interesting/')), app.commands.mark_tag, \
                                 kwargs={'reason':'good','action':'add'}, \
                                 name='mark_interesting_tag'),
 
-    url(r'^%s%s(?P<tag>[^/]+)/$' % (_('mark-tag/'),_('ignored/')), app.commands.mark_tag, \
+                        url(r'^%s%s(?P<tag>[^/]+)/$' % (_('mark-tag/'),_('ignored/')), app.commands.mark_tag, \
                                 kwargs={'reason':'bad','action':'add'}, \
                                 name='mark_ignored_tag'),
 
-    url(r'^%s(?P<tag>[^/]+)/$' % _('unmark-tag/'), app.commands.mark_tag, \
+                        url(r'^%s(?P<tag>[^/]+)/$' % _('unmark-tag/'), app.commands.mark_tag, \
                                 kwargs={'action':'remove'}, \
                                 name='mark_ignored_tag'),
 
 
-
-    url(r'^%s$' % _('users/'),app.users.users, name='users'),
-    url(r'^%s(?P<id>\d+)/%s$' % (_('users/'), _('edit/')), app.users.edit_user, name='edit_user'),
-    url(r'^%s(?P<id>\d+)/%s$' % (_('users/'), _('award/')), app.users.award_points, name='user_award_points'),
-    url(r'^%s(?P<id>\d+)/%s(?P<action>[a-z]+)/(?P<status>[a-z]+)/$' % (_('users/'), _('powers/')), app.users.user_powers, name='user_powers'),
-    url(r'^%s(?P<id>\d+)/%s$' % (_('users/'), _('subscriptions/')), app.users.user_subscriptions, name='user_subscriptions'),
-    url(r'^%s(?P<id>\d+)/(?P<slug>.+)/%s$' % (_('users/'), _('favorites/')), app.users.user_favorites, name='user_favorites'),
-    url(r'^%s(?P<id>\d+)/(?P<slug>.+)/%s$' % (_('users/'), _('reputation/')), app.users.user_reputation, name='user_reputation'),
-    url(r'^%s(?P<id>\d+)/(?P<slug>.+)/%s$' % (_('users/'), _('votes/')), app.users.user_votes, name='user_votes'),
-    url(r'^%s(?P<id>\d+)/(?P<slug>.+)/%s$' % (_('users/'), _('recent/')), app.users.user_recent, name='user_recent'),
-    url(r'^%s(?P<id>\d+)/(?P<slug>.+)/$' % _('users/'), app.users.user_stats, name='user_profile'),
-    
-    url(r'^%s$' % _('badges/'),app.meta.badges, name='badges'),
-    url(r'^%s(?P<id>\d+)/(?P<slug>.+)/$' % _('badges/'), app.meta.badge, name='badge'),
-    # (r'^admin/doc/' % _('admin/doc'), include('django.contrib.admindocs.urls')),
-    url(r'^%s(.*)' % _('nimda/'), admin.site.root, name='osqa_admin'),
-    url(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feeds}, name='feeds'),
-    url(r'^%s$' % _('upload/'), app.writers.upload, name='upload'),
-    url(r'^%s$' % _('search/'), app.readers.search, name='search'),
-    url(r'^%s$' % _('contact/'), app.meta.feedback, name='feedback'),
-
-    (r'^i18n/', include('django.conf.urls.i18n')),
-
-    url(r'^%s%s$' % (_('account/'), _('signin/')), app.auth.signin_page, name='auth_signin'),
-    url(r'^%s%s$' % (_('account/'), _('signout/')), app.auth.signout, name='user_signout'),
-    url(r'^%s%s(?P<action>\w+)/$' % (_('account/'), _('signin/')), app.auth.signin_page, name='auth_action_signin'),
-    url(r'^%s(?P<provider>\w+)/%s$' % (_('account/'), _('signin/')), app.auth.prepare_provider_signin, name='auth_provider_signin'),
-    url(r'^%s(?P<provider>\w+)/%s$' % (_('account/'), _('done/')), app.auth.process_provider_signin, name='auth_provider_done'),
-    url(r'^%s%s$' % (_('account/'), _('register/')), app.auth.external_register, name='auth_external_register'),
-    url(r'^%s%s(?P<user>\d+)/(?P<code>.+)/$' % (_('account/'), _('validate/')),  app.auth.validate_email, name="auth_validate_email"),
-    url(r'^%s%s$' % (_('account/'), _('tempsignin/')),  app.auth.request_temp_login, name="auth_request_tempsignin"),
-    url(r'^%s%s(?P<user>\d+)/(?P<code>.+)/$' % (_('account/'), _('tempsignin/')),  app.auth.temp_signin, name="auth_tempsignin"),
-    url(r'^%s(?P<id>\d+)/%s$' % (_('account/'), _('authsettings/')), app.auth.auth_settings, name='user_authsettings'),
-    url(r'^%s%s(?P<id>\d+)/%s$' % (_('account/'), _('providers/'),  _('remove/')), app.auth.remove_external_provider, name='user_remove_external_provider'),
-    url(r'^%s%s%s$' % (_('account/'), _('providers/'),  _('add/')), app.auth.signin_page, name='user_add_external_provider'),
-
-
-    url(r'^%s$' % _('admin/'), app.admin.dashboard, name="admin_index"),
-    url(r'^%s%s$' % (_('admin/'), _('switch_interface/')), app.admin.interface_switch, name="admin_switch_interface"),
-    url(r'^%s%s$' % (_('admin/'), _('statistics/')), app.admin.statistics, name="admin_statistics"),
-    url(r'^%s%s$' % (_('admin/'), _('denormalize/')), app.admin.recalculate_denormalized, name="admin_denormalize"),
-    url(r'^%s%s$' % (_('admin/'), _('go_bootstrap/')), app.admin.go_bootstrap, name="admin_go_bootstrap"),
-    url(r'^%s%s$' % (_('admin/'), _('go_defaults/')), app.admin.go_defaults, name="admin_go_defaults"),
-    url(r'^%s%s(?P<set_name>\w+)/(?P<var_name>\w+)/$' % (_('admin/'), _('settings/')), app.admin.get_default, name="admin_default"),
-    url(r'^%s%s$' % (_('admin/'), _('maintenance/')), app.admin.maintenance, name="admin_maintenance"),
-    url(r'^%s%s$' % (_('admin/'), _('flagged_posts/')), app.admin.flagged_posts, name="admin_flagged_posts"),
-    url(r'^%s%s$' % (_('admin/'), _('static_pages/')), app.admin.static_pages, name="admin_static_pages"),
-
-    url(r'^%s%s%s$' % (_('admin/'), _('static_pages/'), _('new/')), app.admin.edit_page, name="admin_new_page"),
-    url(r'^%s%s%s(?P<id>\d+)/$' % (_('admin/'), _('static_pages/'), _('edit/')), app.admin.edit_page, name="admin_edit_page"),
-
-    url(r'^%s%s(?P<set_name>\w+)/$' % (_('admin/'), _('settings/')), app.admin.settings_set, name="admin_set"),
-
-    url(r'^feeds/rss/$', RssLastestQuestionsFeed, name="latest_questions_feed"),
-
-    url(r'^(?P<path>.+)$', app.meta.page, name="static_page")
-)
+                        url(r'^%s$' % _('users/'), app.users.users, name='users'),
+                        url(r'^%s(?P<id>\d+)/%s$' % (_('users/'), _('edit/')), app.users.edit_user, name='edit_user'),
+                        url(r'^%s(?P<id>\d+)/%s$' % (_('users/'), _('award/')), app.users.award_points,
+                            name='user_award_points'),
+                        url(r'^%s(?P<id>\d+)/%s$' % (_('users/'), _('suspend/')), app.users.suspend, name='user_suspend'
+                            ),
+                        url(r'^%s(?P<id>\d+)/%s(?P<action>[a-z]+)/(?P<status>[a-z]+)/$' % (_('users/'), _('powers/')),
+                            app.users.user_powers, name='user_powers'),
+                        url(r'^%s(?P<id>\d+)/%s$' % (_('users/'), _('subscriptions/')), app.users.user_subscriptions,
+                            name='user_subscriptions'),
+                        url(r'^%s(?P<id>\d+)/(?P<slug>.+)/%s$' % (_('users/'), _('favorites/')),
+                            app.users.user_favorites, name='user_favorites'),
+                        url(r'^%s(?P<id>\d+)/(?P<slug>.+)/%s$' % (_('users/'), _('reputation/')),
+                            app.users.user_reputation, name='user_reputation'),
+                        url(r'^%s(?P<id>\d+)/(?P<slug>.+)/%s$' % (_('users/'), _('votes/')), app.users.user_votes,
+                            name='user_votes'),
+                        url(r'^%s(?P<id>\d+)/(?P<slug>.+)/%s$' % (_('users/'), _('recent/')), app.users.user_recent,
+                            name='user_recent'),
+                        url(r'^%s(?P<id>\d+)/(?P<slug>.+)/$' % _('users/'), app.users.user_stats, name='user_profile'),
+
+                        url(r'^%s$' % _('badges/'), app.meta.badges, name='badges'),
+                        url(r'^%s(?P<id>\d+)/(?P<slug>.+)/$' % _('badges/'), app.meta.badge, name='badge'),
+                        # (r'^admin/doc/' % _('admin/doc'), include('django.contrib.admindocs.urls')),
+                        url(r'^%s(.*)' % _('nimda/'), admin.site.root, name='osqa_admin'),
+                        url(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feeds},
+                            name='feeds'),
+                        url(r'^%s$' % _('upload/'), app.writers.upload, name='upload'),
+                        url(r'^%s$' % _('search/'), app.readers.search, name='search'),
+                        url(r'^%s$' % _('contact/'), app.meta.feedback, name='feedback'),
+
+                        (r'^i18n/', include('django.conf.urls.i18n')),
+
+                        url(r'^%s%s$' % (_('account/'), _('signin/')), app.auth.signin_page, name='auth_signin'),
+                        url(r'^%s%s$' % (_('account/'), _('signout/')), app.auth.signout, name='user_signout'),
+                        url(r'^%s%s(?P<action>\w+)/$' % (_('account/'), _('signin/')), app.auth.signin_page,
+                            name='auth_action_signin'),
+                        url(r'^%s(?P<provider>\w+)/%s$' % (_('account/'), _('signin/')),
+                            app.auth.prepare_provider_signin, name='auth_provider_signin'),
+                        url(r'^%s(?P<provider>\w+)/%s$' % (_('account/'), _('done/')), app.auth.process_provider_signin,
+                            name='auth_provider_done'),
+                        url(r'^%s%s$' % (_('account/'), _('register/')), app.auth.external_register,
+                            name='auth_external_register'),
+                        url(r'^%s%s(?P<user>\d+)/(?P<code>.+)/$' % (_('account/'), _('validate/')),
+                            app.auth.validate_email, name="auth_validate_email"),
+                        url(r'^%s%s$' % (_('account/'), _('tempsignin/')), app.auth.request_temp_login,
+                            name="auth_request_tempsignin"),
+                        url(r'^%s%s(?P<user>\d+)/(?P<code>.+)/$' % (_('account/'), _('tempsignin/')),
+                            app.auth.temp_signin, name="auth_tempsignin"),
+                        url(r'^%s(?P<id>\d+)/%s$' % (_('account/'), _('authsettings/')), app.auth.auth_settings,
+                            name='user_authsettings'),
+                        url(r'^%s%s(?P<id>\d+)/%s$' % (_('account/'), _('providers/'), _('remove/')),
+                            app.auth.remove_external_provider, name='user_remove_external_provider'),
+                        url(r'^%s%s%s$' % (_('account/'), _('providers/'), _('add/')), app.auth.signin_page,
+                            name='user_add_external_provider'),
+
+
+                        url(r'^%s$' % _('admin/'), app.admin.dashboard, name="admin_index"),
+                        url(r'^%s%s$' % (_('admin/'), _('switch_interface/')), app.admin.interface_switch,
+                            name="admin_switch_interface"),
+                        url(r'^%s%s$' % (_('admin/'), _('statistics/')), app.admin.statistics, name="admin_statistics"),
+                        url(r'^%s%s$' % (_('admin/'), _('denormalize/')), app.admin.recalculate_denormalized,
+                            name="admin_denormalize"),
+                        url(r'^%s%s$' % (_('admin/'), _('go_bootstrap/')), app.admin.go_bootstrap,
+                            name="admin_go_bootstrap"),
+                        url(r'^%s%s$' % (_('admin/'), _('go_defaults/')), app.admin.go_defaults,
+                            name="admin_go_defaults"),
+                        url(r'^%s%s(?P<set_name>\w+)/(?P<var_name>\w+)/$' % (_('admin/'), _('settings/')),
+                            app.admin.get_default, name="admin_default"),
+                        url(r'^%s%s$' % (_('admin/'), _('maintenance/')), app.admin.maintenance,
+                            name="admin_maintenance"),
+                        url(r'^%s%s$' % (_('admin/'), _('flagged_posts/')), app.admin.flagged_posts,
+                            name="admin_flagged_posts"),
+                        url(r'^%s%s$' % (_('admin/'), _('static_pages/')), app.admin.static_pages,
+                            name="admin_static_pages"),
+                        url(r'^%s%s$' % (_('admin/'), _('moderation/')), app.admin.moderation, name="admin_moderation"),
+
+                        url(r'^%s%s%s$' % (_('admin/'), _('static_pages/'), _('new/')), app.admin.edit_page,
+                            name="admin_new_page"),
+                        url(r'^%s%s%s(?P<id>\d+)/$' % (_('admin/'), _('static_pages/'), _('edit/')), app.admin.edit_page
+                            , name="admin_edit_page"),
+
+                        url(r'^%s%s(?P<set_name>\w+)/$' % (_('admin/'), _('settings/')), app.admin.settings_set,
+                            name="admin_set"),
+
+                        url(r'^feeds/rss/$', RssLastestQuestionsFeed, name="latest_questions_feed"),
+
+                        url(r'^(?P<path>.+)$', app.meta.page, name="static_page")
+                        )
index fe2782538df2c85e4d37b07fd4424b1bd7556410..52f9eee3a05a2941aa7260c3e5a5721a513fe825 100644 (file)
@@ -7,7 +7,7 @@ from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidde
 from django.template import RequestContext
 from django.utils.translation import ugettext as _
 from django.utils import simplejson
-from django.db.models import Sum
+from django.db import models
 from forum.settings.base import Setting
 from forum.forms import MaintenanceModeForm, PageForm
 from forum.settings.forms import SettingsSetForm
@@ -258,12 +258,12 @@ def go_defaults(request):
 def recalculate_denormalized(request):
     for n in Node.objects.all():
         n = n.leaf
-        n.score = n.votes.aggregate(score=Sum('value'))['score']
+        n.score = n.votes.aggregate(score=models.Sum('value'))['score']
         if not n.score: n.score = 0
         n.save()
 
     for u in User.objects.all():
-        u.reputation = u.reputes.aggregate(reputation=Sum('value'))['reputation']
+        u.reputation = u.reputes.aggregate(reputation=models.Sum('value'))['reputation']
         u.save()
 
     request.user.message_set.create(message=_('All values recalculated'))
@@ -354,5 +354,78 @@ def edit_page(request, id=None):
     'published': published
     })
 
+@admin_page
+def moderation(request):
+    if request.POST:
+        if not 'ids' in request.POST:
+            verify = None
+        else:
+            sort = {
+            'high-rep': '-reputation',
+            'newer': '-date_joined',
+            'older': 'date_joined',
+            }.get(request.POST.get('sort'), None)
+
+            if sort:
+                try:
+                    limit = int(request.POST['limit'])
+                except:
+                    limit = 5
+
+                verify = User.objects.order_by(sort)[:limit]
+            else:
+                verify = None
+
+        if verify:
+            possible_cheaters = []
+            verify = User.objects.order_by(sort)[:5]
+
+            cheat_score_sort = lambda c1, c2: cmp(c2.fdata['fake_score'], c1.fdata['fake_score'])
+
+            for user in verify:
+                possible_fakes = []
+                affecters = User.objects.filter(actions__node__author=user, actions__canceled=False).annotate(
+                        affect_count=models.Count('actions')).order_by('-affect_count')
+                user_ips = set(Action.objects.filter(user=user).values_list('ip', flat=True).distinct('ip'))
+
+                for affecter in affecters:
+                    if affecter == user:
+                        continue
+
+                    data = {'affect_count': affecter.affect_count}
+
+                    total_actions = affecter.actions.filter(canceled=False).exclude(node=None).count()
+                    ratio = (float(affecter.affect_count) / float(total_actions)) * 100
+
+                    if total_actions > 10 and ratio > 50:
+                        data['total_actions'] = total_actions
+                        data['action_ratio'] = ratio
+
+                        affecter_ips = set(
+                                Action.objects.filter(user=affecter).values_list('ip', flat=True).distinct('ip'))
+                        cross_ips = len(user_ips & affecter_ips)
+
+                        data['cross_ip_count'] = cross_ips
+                        data['total_ip_count'] = len(affecter_ips)
+                        data['cross_ip_ratio'] = (float(data['cross_ip_count']) / float(data['total_ip_count'])) * 100
+
+                        if affecter.email_isvalid:
+                            email_score = 0
+                        else:
+                            email_score = 50.0
+
+                        data['fake_score'] = ((data['cross_ip_ratio'] + data['action_ratio'] + email_score) / 100) * 4
+
+                        affecter.fdata = data
+                        possible_fakes.append(affecter)
+
+                if len(possible_fakes) > 0:
+                    possible_fakes = sorted(possible_fakes, cheat_score_sort)
+                    possible_cheaters.append((user, possible_fakes))
+
+            return ('osqaadmin/moderation.html', {'cheaters': possible_cheaters})
+
+    return ('osqaadmin/moderation.html', {})
+
 
 
index ddc5e95010cfd5030a025d8ca52da01bf6f0851a..27cf5c9198bddbd2aba31aea2e0ad23435a68429 100644 (file)
@@ -9,6 +9,7 @@ from django.utils.http import urlquote_plus
 from django.contrib.auth.decorators import login_required
 from django.contrib.auth import login, logout
 from django.http import get_host
+from forum.actions import SuspendAction
 import types
 import datetime
 import logging
@@ -165,7 +166,8 @@ def external_register(request):
                 auth_provider = request.session['auth_provider']
             except:
                 request.session['auth_error'] = _(
-                        "Oops, something went wrong in the middle of this process. Please try again.")
+                        "Oops, something went wrong in the middle of this process. Please try again. Note that you need to have cookies enabled for the authentication to work."
+                        )
                 logging.error("Missing session data when trying to complete user registration: %s" % ", ".join(
                         ["%s: %s" % (k, v) for k, v in request.META.items()]))
                 return HttpResponseRedirect(reverse('auth_signin'))
@@ -226,6 +228,9 @@ def request_temp_login(request):
         if form.is_valid():
             user = form.user_cache
 
+            if user.is_suspended():
+                return forward_suspended_user(request, user, False)
+
             try:
                 hash = get_object_or_404(ValidationHash, user=user, type='templogin')
                 if hash.expiration < datetime.datetime.now():
@@ -346,7 +351,9 @@ POST_SIGNIN_ACTIONS = {
 }
 
 def login_and_forward(request, user, forward=None, message=None):
-    old_session = request.session.session_key
+    if user.is_suspended():
+        return forward_suspended_user(request, user)
+
     user.backend = "django.contrib.auth.backends.ModelBackend"
     login(request, user)
 
@@ -381,13 +388,21 @@ def login_and_forward(request, user, forward=None, message=None):
     request.user.message_set.create(message=message)
     return HttpResponseRedirect(forward)
 
-@login_required
-def signout(request):
-    """
-    signout from the website. Remove openid from session and kill it.
+def forward_suspended_user(request, user, show_private_msg=True):
+    message = _("Sorry, but this account is suspended")
+    if show_private_msg:
+        msg_type = 'privatemsg'
+    else:
+        msg_type = 'publicmsg'
+
+    suspension = user.suspension
+    if suspension:
+        message += (":<br />" + suspension.extra.get(msg_type, ''))
 
-    url : /signout/"
-    """
+    request.user.message_set.create(message)
+    return HttpResponseRedirect(reverse('index'))
 
+@login_required
+def signout(request):
     logout(request)
     return HttpResponseRedirect(reverse('index'))
\ No newline at end of file
index ed6c03ffbe923b62886c5e4190baf732bf1cbeca..63c65063ea044d8ca82b1aa8b208dbd31df74842 100644 (file)
@@ -13,39 +13,49 @@ from django.core.urlresolvers import reverse
 from django.contrib.auth.decorators import login_required
 from forum.utils.decorators import ajax_method, ajax_login_required
 from forum.modules.decorators import decoratable
-from decorators import command, CommandException
+from decorators import command, CommandException, RefreshPageCommand
 from forum import settings
 import logging
 
 class NotEnoughRepPointsException(CommandException):
     def __init__(self, action):
         super(NotEnoughRepPointsException, self).__init__(
-            _("""Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>""") % {'action': action, 'faq_url': reverse('faq')}
-        )
+                _(
+                        """Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
+                        ) % {'action': action, 'faq_url': reverse('faq')}
+                )
 
 class CannotDoOnOwnException(CommandException):
     def __init__(self, action):
         super(CannotDoOnOwnException, self).__init__(
-            _("""Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>""") % {'action': action, 'faq_url': reverse('faq')}
-        )
+                _(
+                        """Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
+                        ) % {'action': action, 'faq_url': reverse('faq')}
+                )
 
 class AnonymousNotAllowedException(CommandException):
     def __init__(self, action):
         super(AnonymousNotAllowedException, self).__init__(
-            _("""Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>.""") % {'action': action, 'signin_url': reverse('auth_signin')}
-        )
+                _(
+                        """Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>."""
+                        ) % {'action': action, 'signin_url': reverse('auth_signin')}
+                )
 
 class NotEnoughLeftException(CommandException):
     def __init__(self, action, limit):
         super(NotEnoughLeftException, self).__init__(
-            _("""Sorry, but you don't have enough %(action)s left for today..<br />The limit is %(limit)s per day..<br />Please check the <a href='%(faq_url)s'>faq</a>""") % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
-        )
+                _(
+                        """Sorry, but you don't have enough %(action)s left for today..<br />The limit is %(limit)s per day..<br />Please check the <a href='%(faq_url)s'>faq</a>"""
+                        ) % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
+                )
 
 class CannotDoubleActionException(CommandException):
     def __init__(self, action):
         super(CannotDoubleActionException, self).__init__(
-            _("""Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>""") % {'action': action, 'faq_url': reverse('faq')}
-        )
+                _(
+                        """Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
+                        ) % {'action': action, 'faq_url': reverse('faq')}
+                )
 
 
 @command
@@ -76,8 +86,9 @@ def vote_post(request, id, vote_type):
         if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
             raise CommandException(
                     _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
-                    {'ndays': int(settings.DENY_UNVOTE_DAYS), 'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
-            )
+                    {'ndays': int(settings.DENY_UNVOTE_DAYS),
+                     'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
+                    )
 
         old_vote.cancel(ip=request.META['REMOTE_ADDR'])
         score_inc += (old_vote.__class__ == VoteDownAction) and 1 or -1
@@ -89,10 +100,10 @@ def vote_post(request, id, vote_type):
         vote_type = "none"
 
     response = {
-        'commands': {
-            'update_post_score': [id, score_inc],
-            'update_user_post_vote': [id, vote_type]
-        }
+    'commands': {
+    'update_post_score': [id, score_inc],
+    'update_user_post_vote': [id, vote_type]
+    }
     }
 
     votes_left = (int(settings.MAX_VOTES_PER_DAY) - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
@@ -127,7 +138,8 @@ def flag_post(request, id):
 
     try:
         current = FlagAction.objects.get(canceled=False, user=user, node=post)
-        raise CommandException(_("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
+        raise CommandException(
+                _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
     except ObjectDoesNotExist:
         reason = request.POST.get('prompt', '').strip()
 
@@ -137,7 +149,7 @@ def flag_post(request, id):
         FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
 
     return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
-        
+
 @command
 def like_comment(request, id):
     comment = get_object_or_404(Comment, id=id)
@@ -150,7 +162,7 @@ def like_comment(request, id):
         raise CannotDoOnOwnException(_('like'))
 
     if not user.can_like_comment(comment):
-        raise NotEnoughRepPointsException( _('like comments'))    
+        raise NotEnoughRepPointsException( _('like comments'))
 
     like = VoteAction.get_action_for(node=comment, user=user)
 
@@ -162,10 +174,10 @@ def like_comment(request, id):
         likes = True
 
     return {
-        'commands': {
-            'update_post_score': [comment.id, likes and 1 or -1],
-            'update_user_post_vote': [comment.id, likes and 'up' or 'none']
-        }
+    'commands': {
+    'update_post_score': [comment.id, likes and 1 or -1],
+    'update_user_post_vote': [comment.id, likes and 'up' or 'none']
+    }
     }
 
 @command
@@ -183,9 +195,9 @@ def delete_comment(request, id):
         DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
 
     return {
-        'commands': {
-            'remove_comment': [comment.id],
-        }
+    'commands': {
+    'remove_comment': [comment.id],
+    }
     }
 
 @command
@@ -204,10 +216,10 @@ def mark_favorite(request, id):
         added = True
 
     return {
-        'commands': {
-            'update_favorite_count': [added and 1 or -1],
-            'update_favorite_mark': [added and 'on' or 'off']
-        }
+    'commands': {
+    'update_favorite_count': [added and 1 or -1],
+    'update_favorite_mark': [added and 'on' or 'off']
+    }
     }
 
 @decoratable
@@ -239,27 +251,30 @@ def comment(request, id):
         if not user.can_edit_comment(comment):
             raise NotEnoughRepPointsException( _('edit comments'))
 
-        comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(text=comment_text)).node
+        comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
+                data=dict(text=comment_text)).node
     else:
         if not user.can_comment(post):
             raise NotEnoughRepPointsException( _('comment'))
 
-        comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(data=dict(text=comment_text, parent=post)).node
+        comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
+                data=dict(text=comment_text, parent=post)).node
 
     if comment.active_revision.revision == 1:
         return {
-            'commands': {
-                'insert_comment': [
-                    id, comment.id, comment.comment, user.username, user.get_profile_url(),
-                        reverse('delete_comment', kwargs={'id': comment.id}), reverse('node_markdown', kwargs={'id': comment.id})
+        'commands': {
+        'insert_comment': [
+                id, comment.id, comment.comment, user.username, user.get_profile_url(),
+                reverse('delete_comment', kwargs={'id': comment.id}),
+                reverse('node_markdown', kwargs={'id': comment.id})
                 ]
-            }
+        }
         }
     else:
         return {
-            'commands': {
-                'update_comment': [comment.id, comment.comment]
-            }
+        'commands': {
+        'update_comment': [comment.id, comment.comment]
+        }
         }
 
 @command
@@ -303,7 +318,7 @@ def accept_answer(request, id):
 
     return {'commands': commands}
 
-@command    
+@command
 def delete_post(request, id):
     post = get_object_or_404(Node, id=id)
     user = request.user
@@ -353,11 +368,7 @@ def close(request, id, close):
 
         CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
 
-    return {
-        'commands': {
-            'refresh_page': []
-        }
-    }
+    return RefreshPageCommand()
 
 @command
 def wikify(request, id):
@@ -381,11 +392,7 @@ def wikify(request, id):
 
         WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
 
-    return {
-        'commands': {
-            'refresh_page': []
-        }
-    }
+    return RefreshPageCommand()
 
 @command
 def convert_to_comment(request, id):
@@ -394,9 +401,11 @@ def convert_to_comment(request, id):
     question = answer.question
 
     if not request.POST:
-        description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': a.author.username, 'snippet': a.summary[:10]}
+        description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': a.author.username,
+                                                                            'snippet': a.summary[:10]}
         nodes = [(question.id, _("Question"))]
-        [nodes.append((a.id, description(a))) for a in question.answers.filter_state(deleted=False).exclude(id=answer.id)]
+        [nodes.append((a.id, description(a))) for a in
+         question.answers.filter_state(deleted=False).exclude(id=answer.id)]
 
         return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
 
@@ -416,11 +425,7 @@ def convert_to_comment(request, id):
 
     AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
 
-    return {
-        'commands': {
-            'refresh_page': []
-        }
-    }
+    return RefreshPageCommand()
 
 @command
 def subscribe(request, id):
@@ -436,10 +441,10 @@ def subscribe(request, id):
         subscribed = True
 
     return {
-        'commands': {
-                'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
-                'set_subscription_status': ['']
-            }
+    'commands': {
+    'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
+    'set_subscription_status': ['']
+    }
     }
 
 #internally grouped views - used by the tagging system
@@ -465,20 +470,21 @@ def mark_tag(request, tag=None, **kwargs):#tagging system
 
 def matching_tags(request):
     if len(request.GET['q']) == 0:
-       raise CommandException(_("Invalid request"))
+        raise CommandException(_("Invalid request"))
 
     possible_tags = Tag.active.filter(name__istartswith = request.GET['q'])
     tag_output = ''
     for tag in possible_tags:
         tag_output += (tag.name + "|" + tag.name + "." + tag.used_count.__str__() + "\n")
-        
+
     return HttpResponse(tag_output, mimetype="text/plain")
 
 def related_questions(request):
     if request.POST and request.POST.get('title', None):
         return HttpResponse(simplejson.dumps(
                 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
-                 for q in Question.objects.search(request.POST['title']).filter_state(deleted=False)[0:10]]), mimetype="application/json")
+                 for q in Question.objects.search(request.POST['title']).filter_state(deleted=False)[0:10]]),
+                            mimetype="application/json")
     else:
         raise Http404()
 
index 37413d29e6872039ef10830ef3746b7b900b1ea9..db6dec48e9712f194cac483bdf74e5c49a72c6a2 100644 (file)
@@ -14,8 +14,11 @@ def render(template=None, tab=None):
             if tab is not None:\r
                 context['tab'] = tab\r
 \r
-            return render_to_response(context.pop('template', template), context, context_instance=RequestContext(request))\r
+            return render_to_response(context.pop('template', template), context,\r
+                                      context_instance=RequestContext(request))\r
+\r
         return decorated\r
+\r
     return decorator\r
 \r
 def list(paginate, default_page_size):\r
@@ -33,37 +36,44 @@ def list(paginate, default_page_size):
                 page_obj = paginator.page(page)\r
             except EmptyPage:\r
                 raise Http404()\r
-                \r
+\r
             context[paginate] = page_obj.object_list.lazy()\r
 \r
             base_path = context.get('base_path', None) or request.path\r
             sort = request.utils.sort_method('')\r
 \r
             context["pagination_context"] = {\r
-                'is_paginated' : True,\r
-                'pages': paginator.num_pages,\r
-                'page': page,\r
-                'has_previous': page_obj.has_previous(),\r
-                'has_next': page_obj.has_next(),\r
-                'previous': page_obj.previous_page_number(),\r
-                'next': page_obj.next_page_number(),\r
-                'base_url' : "%s%ssort=%s&" % (base_path, ('?' in base_path) and '&' or '?', sort),\r
-                'pagesize' : pagesize\r
+            'is_paginated' : True,\r
+            'pages': paginator.num_pages,\r
+            'page': page,\r
+            'has_previous': page_obj.has_previous(),\r
+            'has_next': page_obj.has_next(),\r
+            'previous': page_obj.previous_page_number(),\r
+            'next': page_obj.next_page_number(),\r
+            'base_url' : "%s%ssort=%s&" % (base_path, ('?' in base_path) and '&' or '?', sort),\r
+            'pagesize' : pagesize\r
             }\r
 \r
             context['sort_context'] = {\r
-                'base_url': "%s%ssort=" % (base_path, ('?' in base_path) and '&' or '?'),\r
-                'current': sort,\r
+            'base_url': "%s%ssort=" % (base_path, ('?' in base_path) and '&' or '?'),\r
+            'current': sort,\r
             }\r
 \r
             return context\r
+\r
         return decorated\r
+\r
     return decorator\r
 \r
 \r
 class CommandException(Exception):\r
     pass\r
 \r
+class RefreshPageCommand(HttpResponse):\r
+    def __init__(self):\r
+        super(RefreshPageCommand, self).__init__(\r
+                content=simplejson.dumps({'commands': {'refresh_page': []}, 'success': True}),\r
+                mimetype="application/json")\r
 \r
 def command(func):\r
     def decorated(request, *args, **kwargs):\r
@@ -75,19 +85,20 @@ def command(func):
 \r
             response['success'] = True\r
         except Exception, e:\r
-            #import sys, traceback\r
-            #traceback.print_exc(file=sys.stdout)\r
+            import traceback\r
+            #traceback.print_exc()\r
 \r
             if isinstance(e, CommandException):\r
                 response = {\r
-                    'success': False,\r
-                    'error_message': e.message\r
+                'success': False,\r
+                'error_message': e.message\r
                 }\r
             else:\r
                 logging.error("%s: %s" % (func.__name__, str(e)))\r
+                logging.error(traceback.format_exc())\r
                 response = {\r
-                    'success': False,\r
-                    'error_message': _("We're sorry, but an unknown error ocurred.<br />Please try again in a while.")\r
+                'success': False,\r
+                'error_message': _("We're sorry, but an unknown error ocurred.<br />Please try again in a while.")\r
                 }\r
 \r
         if request.is_ajax():\r
index da556be2654706cf0124786f99bd6b7663142f7e..fbde0954dd89fc0416383773bafe095d4c504468 100644 (file)
@@ -17,7 +17,7 @@ from forum.forms import *
 from forum.utils.html import sanitize_html\r
 from datetime import datetime, date\r
 import decorators\r
-from forum.actions import EditProfileAction, FavoriteAction, BonusRepAction\r
+from forum.actions import EditProfileAction, FavoriteAction, BonusRepAction, SuspendAction\r
 \r
 import time\r
 \r
@@ -26,7 +26,7 @@ USERS_PAGE_SIZE = 35# refactor - move to some constants file
 def users(request):\r
     is_paginated = True\r
     sortby = request.GET.get('sort', 'reputation')\r
-    suser = request.REQUEST.get('q',  "")\r
+    suser = request.REQUEST.get('q', "")\r
     try:\r
         page = int(request.GET.get('page', '1'))\r
     except ValueError:\r
@@ -41,11 +41,12 @@ def users(request):
             objects_list = Paginator(User.objects.all().order_by('username'), USERS_PAGE_SIZE)\r
         # default\r
         else:\r
-            objects_list = Paginator(User.objects.all().order_by('-reputation'), USERS_PAGE_SIZE)\r
+            objects_list = Paginator(User.objects.all().order_by('-is_active', '-reputation'), USERS_PAGE_SIZE)\r
         base_url = reverse('users') + '?sort=%s&' % sortby\r
     else:\r
         sortby = "reputation"\r
-        objects_list = Paginator(User.objects.filter(username__icontains=suser).order_by('-reputation'), USERS_PAGE_SIZE)\r
+        objects_list = Paginator(User.objects.filter(username__icontains=suser).order_by('-reputation'), USERS_PAGE_SIZE\r
+                                 )\r
         base_url = reverse('users') + '?name=%s&sort=%s&' % (suser, sortby)\r
 \r
     try:\r
@@ -54,30 +55,30 @@ def users(request):
         users = objects_list.page(objects_list.num_pages)\r
 \r
     return render_to_response('users/users.html', {\r
-                                "users" : users,\r
-                                "suser" : suser,\r
-                                "keywords" : suser,\r
-                                "tab_id" : sortby,\r
-                                "context" : {\r
-                                    'is_paginated' : is_paginated,\r
-                                    'pages': objects_list.num_pages,\r
-                                    'page': page,\r
-                                    'has_previous': users.has_previous(),\r
-                                    'has_next': users.has_next(),\r
-                                    'previous': users.previous_page_number(),\r
-                                    'next': users.next_page_number(),\r
-                                    'base_url' : base_url\r
-                                }\r
-\r
-                                }, context_instance=RequestContext(request))\r
+    "users" : users,\r
+    "suser" : suser,\r
+    "keywords" : suser,\r
+    "tab_id" : sortby,\r
+    "context" : {\r
+    'is_paginated' : is_paginated,\r
+    'pages': objects_list.num_pages,\r
+    'page': page,\r
+    'has_previous': users.has_previous(),\r
+    'has_next': users.has_next(),\r
+    'previous': users.previous_page_number(),\r
+    'next': users.next_page_number(),\r
+    'base_url' : base_url\r
+    }\r
+\r
+    }, context_instance=RequestContext(request))\r
 \r
 def set_new_email(user, new_email, nomessage=False):\r
     if new_email != user.email:\r
         user.email = new_email\r
         user.email_isvalid = False\r
         user.save()\r
-        #if settings.EMAIL_VALIDATION == 'on':\r
-        #    send_new_email_key(user,nomessage=nomessage)    \r
+    #if settings.EMAIL_VALIDATION == 'on':\r
+    #    send_new_email_key(user,nomessage=nomessage)\r
 \r
 @login_required\r
 def edit_user(request, id):\r
@@ -108,10 +109,10 @@ def edit_user(request, id):
     else:\r
         form = EditUserForm(user)\r
     return render_to_response('users/edit.html', {\r
-                                                'user': user,\r
-                                                'form' : form,\r
-                                                'gravatar_faq_url' : reverse('faq') + '#gravatar',\r
-                                    }, context_instance=RequestContext(request))\r
+    'user': user,\r
+    'form' : form,\r
+    'gravatar_faq_url' : reverse('faq') + '#gravatar',\r
+    }, context_instance=RequestContext(request))\r
 \r
 \r
 @login_required\r
@@ -129,7 +130,7 @@ def user_powers(request, id, action, status):
     else:\r
         raise Http404()\r
 \r
-    user.save()    \r
+    user.save()\r
     return HttpResponseRedirect(user.get_profile_url())\r
 \r
 \r
@@ -151,6 +152,38 @@ def award_points(request, id):
     return dict(reputation=user.reputation)\r
 \r
 \r
+@decorators.command\r
+def suspend(request, id):\r
+    user = get_object_or_404(User, id=id)\r
+\r
+    if not request.POST:\r
+        if user.is_suspended():\r
+            suspension = user.suspension\r
+            suspension.cancel(ip=request.META['REMOTE_ADDR'])\r
+            return decorators.RefreshPageCommand()\r
+        else:\r
+            return render_to_response('users/suspend_user.html')\r
+\r
+    if not request.user.is_superuser:\r
+        raise decorators.CommandException(_("Only superusers can ban other users"))\r
+\r
+    data = {\r
+    'bantype': request.POST.get('bantype', 'indefinetly').strip(),\r
+    'publicmsg': request.POST.get('publicmsg', _('Bad behaviour')),\r
+    'privatemsg': request.POST.get('privatemsg', None) or request.POST.get('publicmsg', ''),\r
+    'suspender': request.user.id\r
+    }\r
+\r
+    if data['bantype'] == 'forxdays':\r
+        try:\r
+            data['forxdays'] = int(request.POST['forxdays'])\r
+        except:\r
+            raise decorators.CommandException(_('Invalid numeric argument for the number of days.'))\r
+\r
+    SuspendAction(user=user, ip=request.META['REMOTE_ADDR']).save(data=data)\r
+\r
+    return decorators.RefreshPageCommand()\r
+\r
 def user_view(template, tab_name, tab_description, page_title, private=False):\r
     def decorator(fn):\r
         def decorated(request, id, slug=None):\r
@@ -162,13 +195,15 @@ def user_view(template, tab_name, tab_description, page_title, private=False):
             rev_page_title = user.username + " - " + page_title\r
 \r
             context.update({\r
-                "tab_name" : tab_name,\r
-                "tab_description" : tab_description,\r
-                "page_title" : rev_page_title,\r
-                "can_view_private": (user == request.user) or request.user.is_superuser\r
+            "tab_name" : tab_name,\r
+            "tab_description" : tab_description,\r
+            "page_title" : rev_page_title,\r
+            "can_view_private": (user == request.user) or request.user.is_superuser\r
             })\r
             return render_to_response(template, context, context_instance=RequestContext(request))\r
+\r
         return decorated\r
+\r
     return decorator\r
 \r
 \r
@@ -186,32 +221,35 @@ def user_stats(request, user):
         .annotate(user_tag_usage_count=Count('name')).order_by('-user_tag_usage_count')\r
 \r
     awards = [(Badge.objects.get(id=b['id']), b['count']) for b in\r
-            Badge.objects.filter(awards__user=user).values('id').annotate(count=Count('cls')).order_by('-count')]\r
+              Badge.objects.filter(awards__user=user).values('id').annotate(count=Count('cls')).order_by('-count')]\r
 \r
     return {\r
-            "view_user" : user,\r
-            "questions" : questions,\r
-            "answers" : answers,\r
-            "up_votes" : up_votes,\r
-            "down_votes" : down_votes,\r
-            "total_votes": up_votes + down_votes,\r
-            "votes_today_left": votes_total-votes_today,\r
-            "votes_total_per_day": votes_total,\r
-            "user_tags" : user_tags[:50],\r
-            "awards": awards,\r
-            "total_awards" : len(awards),\r
-        }\r
+    "view_user" : user,\r
+    "questions" : questions,\r
+    "answers" : answers,\r
+    "up_votes" : up_votes,\r
+    "down_votes" : down_votes,\r
+    "total_votes": up_votes + down_votes,\r
+    "votes_today_left": votes_total-votes_today,\r
+    "votes_total_per_day": votes_total,\r
+    "user_tags" : user_tags[:50],\r
+    "awards": awards,\r
+    "total_awards" : len(awards),\r
+    }\r
 \r
 @user_view('users/recent.html', 'recent', _('recent user activity'), _('recent activity'))\r
 def user_recent(request, user):\r
-    activities = user.actions.exclude(action_type__in=("voteup", "votedown", "voteupcomment", "flag", "newpage", "editpage")).order_by('-action_date')[:USERS_PAGE_SIZE]\r
+    activities = user.actions.exclude(\r
+            action_type__in=("voteup", "votedown", "voteupcomment", "flag", "newpage", "editpage")).order_by(\r
+            '-action_date')[:USERS_PAGE_SIZE]\r
 \r
     return {"view_user" : user, "activities" : activities}\r
 \r
 \r
 @user_view('users/votes.html', 'votes', _('user vote record'), _('votes'), True)\r
 def user_votes(request, user):\r
-    votes = user.votes.exclude(node__state_string__contains="(deleted").filter(node__node_type__in=("question", "answer")).order_by('-voted_at')[:USERS_PAGE_SIZE]\r
+    votes = user.votes.exclude(node__state_string__contains="(deleted").filter(\r
+            node__node_type__in=("question", "answer")).order_by('-voted_at')[:USERS_PAGE_SIZE]\r
 \r
     return {"view_user" : user, "votes" : votes}\r
 \r
@@ -220,18 +258,18 @@ def user_votes(request, user):
 def user_reputation(request, user):\r
     rep = list(user.reputes.order_by('date'))\r
     values = [r.value for r in rep]\r
-    redux = lambda x, y: x+y     \r
+    redux = lambda x, y: x+y\r
 \r
     graph_data = simplejson.dumps([\r
-            (time.mktime(rep[i].date.timetuple()) * 1000, reduce(redux, values[:i], 0))\r
-            for i in range(len(values))\r
+    (time.mktime(rep[i].date.timetuple()) * 1000, reduce(redux, values[:i], 0))\r
+    for i in range(len(values))\r
     ])\r
 \r
     rep = user.reputes.filter(action__canceled=False).order_by('-date')[0:20]\r
-    \r
+\r
     return {"view_user": user, "reputation": rep, "graph_data": graph_data}\r
 \r
-@user_view('users/questions.html', 'favorites', _('favorite questions'),  _('favorite questions'))\r
+@user_view('users/questions.html', 'favorites', _('favorite questions'), _('favorite questions'))\r
 def user_favorites(request, user):\r
     favorites = FavoriteAction.objects.filter(canceled=False, user=user)\r
 \r
@@ -252,7 +290,7 @@ def user_subscriptions(request, user):
                 request.user.message_set.create(message=_('Notifications are now disabled'))\r
 \r
         form.is_valid()\r
-        for k,v in form.cleaned_data.items():\r
+        for k, v in form.cleaned_data.items():\r
             setattr(user.subscription_settings, k, v)\r
 \r
         user.subscription_settings.save()\r
@@ -271,7 +309,7 @@ def account_settings(request):
     is_openid = False\r
 \r
     return render_to_response('account_settings.html', {\r
-        'msg': msg,\r
-        'is_openid': is_openid\r
-        }, context_instance=RequestContext(request))\r
+    'msg': msg,\r
+    'is_openid': is_openid\r
+    }, context_instance=RequestContext(request))\r
 \r
index 961c7da389674583cbe1d923b429839f94e5fc7e..9529bbaa24fcea3feeb36de01eb6f8d2e05cdefd 100644 (file)
@@ -1,4 +1,4 @@
-from forum.forms import NextUrlField,  UserNameField,  UserEmailField, SetPasswordForm
+from forum.forms import NextUrlField, UserNameField, UserEmailField, SetPasswordForm
 from forum.models import Question
 from forum.modules import call_all_handlers
 from django.contrib.contenttypes.models import ContentType
@@ -20,7 +20,7 @@ class ClassicRegisterForm(SetPasswordForm):
         spam_fields = call_all_handlers('create_anti_spam_field')
         if spam_fields:
             spam_fields = dict(spam_fields)
-            for name,field in spam_fields.items():
+            for name, field in spam_fields.items():
                 self.fields[name] = field
 
             self._anti_spam_fields = spam_fields.keys()
@@ -33,18 +33,18 @@ class ClassicRegisterForm(SetPasswordForm):
 class ClassicLoginForm(forms.Form):
     """ legacy account signin form """
     next = NextUrlField()
-    username = UserNameField(required=False,skip_clean=True)
+    username = UserNameField(required=False, skip_clean=True)
     password = forms.CharField(max_length=128,
-            widget=forms.widgets.PasswordInput(attrs={'class':'required login'}),
-            required=False)
+                               widget=forms.widgets.PasswordInput(attrs={'class':'required login'}),
+                               required=False)
 
     def __init__(self, data=None, files=None, auto_id='id_%s',
-            prefix=None, initial=None):
+                 prefix=None, initial=None):
         super(ClassicLoginForm, self).__init__(data, files, auto_id,
-                prefix, initial)
+                                               prefix, initial)
         self.user_cache = None
 
-    def _clean_nonempty_field(self,field):
+    def _clean_nonempty_field(self, field):
         value = None
         if field in self.cleaned_data:
             value = self.cleaned_data[field].strip()
@@ -72,13 +72,11 @@ class ClassicLoginForm(forms.Form):
                 del self.cleaned_data['username']
                 del self.cleaned_data['password']
 
-                error_list.insert(0,(_("Please enter valid username and password "
-                                    "(both are case-sensitive).")))
+                error_list.insert(0, (_("Please enter valid username and password "
+                "(both are case-sensitive).")))
 
-            elif not user_.is_active:
-                error_list.append(_("This account is inactive."))
             if len(error_list) > 0:
-                error_list.insert(0,_('Login failed.'))
+                error_list.insert(0, _('Login failed.'))
             try:
                 self.user_cache = user_.user
             except: