Browse Source

#474: refactored bans

Rafał Pitoń 10 years ago
parent
commit
887f4ace14
34 changed files with 498 additions and 201 deletions
  1. 2 0
      misago/conf/defaults.py
  2. 61 0
      misago/static/misago/admin/css/misago/datetimepicker.less
  3. 1 0
      misago/static/misago/admin/css/misago/misago.less
  4. 65 0
      misago/static/misago/admin/js/misago-datetimepicker.js
  5. 61 0
      misago/static/misago/css/misago/datepicker.less
  6. 1 0
      misago/static/misago/css/misago/misago.less
  7. 1 3
      misago/static/misago/css/style.less
  8. 65 0
      misago/static/misago/js/misago-datetimepicker.js
  9. 1 1
      misago/templates/bootstrap3/field.html
  10. 2 2
      misago/templates/misago/admin/bans/form.html
  11. 8 6
      misago/templates/misago/admin/bans/list.html
  12. 1 1
      misago/templates/misago/admin/users/ban.html
  13. 1 1
      misago/templates/misago/modusers/ban.html
  14. 3 3
      misago/templates/misago/profile/ban_details.html
  15. 1 1
      misago/templates/misago/user_state.html
  16. 26 25
      misago/users/bans.py
  17. 4 4
      misago/users/decorators.py
  18. 24 28
      misago/users/forms/admin.py
  19. 2 3
      misago/users/forms/auth.py
  20. 4 4
      misago/users/forms/modusers.py
  21. 4 5
      misago/users/management/commands/bansmaintenance.py
  22. 4 4
      misago/users/migrations/0001_initial.py
  23. 67 38
      misago/users/models/ban.py
  24. 4 1
      misago/users/online/utils.py
  25. 4 2
      misago/users/tests/test_activation_views.py
  26. 2 1
      misago/users/tests/test_auth_views.py
  27. 31 31
      misago/users/tests/test_ban_model.py
  28. 10 9
      misago/users/tests/test_banadmin_views.py
  29. 18 8
      misago/users/tests/test_bans.py
  30. 10 10
      misago/users/tests/test_bansmaintenance.py
  31. 4 2
      misago/users/tests/test_forgottenpassword_views.py
  32. 2 6
      misago/users/tests/test_validators.py
  33. 1 1
      misago/users/views/admin/users.py
  34. 3 1
      misago/users/views/register.py

+ 2 - 0
misago/conf/defaults.py

@@ -67,6 +67,7 @@ PIPELINE_JS = {
             'misago/js/misago-uiserver.js',
             'misago/js/misago-bindings.js',
             'misago/js/misago-tooltips.js',
+            'misago/js/misago-datetimepicker.js',
             'misago/js/misago-yesnoswitch.js',
             'misago/js/misago-dropdowns.js',
             'misago/js/misago-modal.js',
@@ -92,6 +93,7 @@ PIPELINE_JS = {
             'misago/admin/js/bootstrap.js',
             'misago/admin/js/moment.min.js',
             'misago/admin/js/bootstrap-datetimepicker.min.js',
+            'misago/admin/js/misago-datetimepicker.js',
             'misago/admin/js/misago-timestamps.js',
             'misago/admin/js/misago-tooltips.js',
             'misago/admin/js/misago-tables.js',

+ 61 - 0
misago/static/misago/admin/css/misago/datetimepicker.less

@@ -0,0 +1,61 @@
+//
+// Date and time picker
+// --------------------------------------------------
+
+
+.bootstrap-datetimepicker-widget {
+  padding: @line-height-computed / 2;
+  width: auto;
+
+  &.top:after, &.top:before, &.bottom:after, &.bottom:before {
+    display: none;
+  }
+
+  .timepicker-picker {
+    table.table-condensed {
+      margin: 0px;
+
+      tr {
+        td {
+          padding: 0px;
+          height: auto;
+
+          text-align: center;
+
+          &.separator {
+            width: @line-height-computed / 2;
+            height: auto;
+            line-height: @line-height-computed * 1.5;
+          }
+
+          .btn {
+            border: none;
+            display: block;
+            margin: 0px;
+            padding: 0px;
+            position: static;
+            width: 100%;
+
+            .glyphicon {
+              margin: 0px;
+              height: @line-height-computed * 1.5;
+              width: 100%;
+              position: static;
+
+              line-height: @line-height-computed * 1.5;
+            }
+          }
+
+          .timepicker-hour, .timepicker-minute {
+            margin: 0px;
+            float: none;
+            height: auto;
+            width: auto;
+
+            line-height: @line-height-computed * 1.5;
+          }
+        }
+      }
+    }
+  }
+}

+ 1 - 0
misago/static/misago/admin/css/misago/misago.less

@@ -12,6 +12,7 @@ html, body {
 @import "pager.less";
 @import "tables.less";
 @import "lists.less";
+@import "datetimepicker.less";
 @import "yesnoswitch.less";
 
 // Layout elements

+ 65 - 0
misago/static/misago/admin/js/misago-datetimepicker.js

@@ -0,0 +1,65 @@
+// Form enchancer for datetimes
+$(function() {
+
+  function enchanceDateTimeField($control) {
+
+    var formats = $control.data('input-format').split(" ");
+    var date_format = formats[0];
+    var time_format = formats[1].replace(":%S", "");
+
+    var $input = $control.find('input');
+
+    $input.attr('type', 'hidden');
+
+    var $date = $('<input type="text"  class="form-control" style="float: left; width: 96px; margin-right: 8px;">');
+    var $time = $('<input type="text" class="form-control" style="float: left; width: 64px;">');
+
+    var $container = $('<div style="overflow: auto;"></div>');
+
+    $container.insertAfter($input);
+
+    $container.append($date);
+    $container.append($time);
+
+    date_format = date_format.replace('%d', 'DD');
+    date_format = date_format.replace('%m', 'MM');
+    date_format = date_format.replace('%y', 'YY');
+    date_format = date_format.replace('%Y', 'YYYY');
+
+    $date.datetimepicker({
+      format: date_format,
+      pickDate: true,
+      pickTime: false
+    });
+
+    var values = $input.val().split(" ");
+    if (values) {
+      $date.val(values[0]);
+
+      var time = values[1].split(":");
+      $time.val(time[0] + ":" + time[1]);
+    }
+
+    $time.datetimepicker({
+      format: 'HH:mm',
+      pickDate: false,
+      pickSeconds: false,
+      pick12HourFormat: false
+    });
+
+    function update_value() {
+      $input.val($date.val() + " " + $time.val());
+    }
+
+    $date.change(update_value);
+    $time.change(update_value);
+  }
+
+  // discover formatted fields
+  $('.controls').each(function() {
+    if ($(this).data('input-format')) {
+      enchanceDateTimeField($(this));
+    }
+  })
+
+})

+ 61 - 0
misago/static/misago/css/misago/datepicker.less

@@ -0,0 +1,61 @@
+//
+// Date and time picker
+// --------------------------------------------------
+
+
+.bootstrap-datetimepicker-widget {
+  padding: @line-height-computed / 2;
+  width: auto;
+
+  &.top:after, &.top:before, &.bottom:after, &.bottom:before {
+    display: none;
+  }
+
+  .timepicker-picker {
+    table.table-condensed {
+      margin: 0px;
+
+      tr {
+        td {
+          padding: 0px;
+          height: auto;
+
+          text-align: center;
+
+          &.separator {
+            width: @line-height-computed / 2;
+            height: auto;
+            line-height: @line-height-computed * 1.5;
+          }
+
+          .btn {
+            border: none;
+            display: block;
+            margin: 0px;
+            padding: 0px;
+            position: static;
+            width: 100%;
+
+            .glyphicon {
+              margin: 0px;
+              height: @line-height-computed * 1.5;
+              width: 100%;
+              position: static;
+
+              line-height: @line-height-computed * 1.5;
+            }
+          }
+
+          .timepicker-hour, .timepicker-minute {
+            margin: 0px;
+            float: none;
+            height: auto;
+            width: auto;
+
+            line-height: @line-height-computed * 1.5;
+          }
+        }
+      }
+    }
+  }
+}

+ 1 - 0
misago/static/misago/css/misago/misago.less

@@ -15,6 +15,7 @@
 @import "tables.less";
 @import "threadslists.less";
 @import "typography.less";
+@import "datepicker.less";
 @import "yesnoswitch.less";
 
 // Layout elements

+ 1 - 3
misago/static/misago/css/style.less

@@ -12,14 +12,12 @@
 // 3rd party libs
 @import "font-awesome.css";
 @import "jquery.Jcrop.css";
+@import "bootstrap-datetimepicker.less";
 
 // Import other files
 @import "bootstrap/bootstrap.less";
 @import "misago/misago.less";
 @import "flavor/flavor.less";
 
-// Bootstrap 3rd party libs
-@import "bootstrap-datetimepicker.less";
-
 // Rank overrides
 @import "ranks.less";

+ 65 - 0
misago/static/misago/js/misago-datetimepicker.js

@@ -0,0 +1,65 @@
+// Form enchancer for datetimes
+$(function() {
+
+  function enchanceDateTimeField($control) {
+
+    var formats = $control.data('input-format').split(" ");
+    var date_format = formats[0];
+    var time_format = formats[1].replace(":%S", "");
+
+    var $input = $control.find('input');
+
+    $input.attr('type', 'hidden');
+
+    var $date = $('<input type="text"  class="form-control" style="float: left; width: 96px; margin-right: 8px;">');
+    var $time = $('<input type="text" class="form-control" style="float: left; width: 64px;">');
+
+    var $container = $('<div style="overflow: auto;"></div>');
+
+    $container.insertAfter($input);
+
+    $container.append($date);
+    $container.append($time);
+
+    date_format = date_format.replace('%d', 'DD');
+    date_format = date_format.replace('%m', 'MM');
+    date_format = date_format.replace('%y', 'YY');
+    date_format = date_format.replace('%Y', 'YYYY');
+
+    $date.datetimepicker({
+      format: date_format,
+      pickDate: true,
+      pickTime: false
+    });
+
+    var values = $input.val().split(" ");
+    if (values) {
+      $date.val(values[0]);
+
+      var time = values[1].split(":");
+      $time.val(time[0] + ":" + time[1]);
+    }
+
+    $time.datetimepicker({
+      format: 'HH:mm',
+      pickDate: false,
+      pickSeconds: false,
+      pick12HourFormat: false
+    });
+
+    function update_value() {
+      $input.val($date.val() + " " + $time.val());
+    }
+
+    $date.change(update_value);
+    $time.change(update_value);
+  }
+
+  // discover formatted fields
+  $('.controls').each(function() {
+    if ($(this).data('input-format')) {
+      enchanceDateTimeField($(this));
+    }
+  })
+
+})

+ 1 - 1
misago/templates/bootstrap3/field.html

@@ -35,7 +35,7 @@
                     </div>
                 {% endif %}
             {% else %}
-                <div class="controls {{ field_class }}">
+                <div class="controls {{ field_class }}"{% if field.field.input_formats %} data-input-format="{{ field.field.input_formats.0 }}"{% endif %}>
                     {% crispy_field field %}
                     {% include 'bootstrap3/layout/help_text_and_errors.html' %}
                 </div>

+ 2 - 2
misago/templates/misago/admin/bans/form.html

@@ -42,9 +42,9 @@ class="form-horizontal"
   <fieldset>
     <legend>{% trans "Ban settings" %}</legend>
 
-    {% form_row form.test label_class field_class %}
+    {% form_row form.check_type label_class field_class %}
     {% form_row form.banned_value label_class field_class %}
-    {% form_row form.valid_until label_class field_class %}
+    {% form_row form.expires_on label_class field_class %}
 
   </fieldset>
   <fieldset>

+ 8 - 6
misago/templates/misago/admin/bans/list.html

@@ -15,7 +15,7 @@
 {% block table-header %}
 <th style="width: 25%;">{% trans "Ban" %}</th>
 <th style="width: 160px;">{% trans "Type" %}</th>
-<th>{% trans "Valid until" %}</th>
+<th>{% trans "Expires on" %}</th>
 {% for action in extra_actions %}
 <th style="width: 1%;">&nbsp;</th>
 {% endfor %}
@@ -32,17 +32,19 @@
   {{ item.test_name }}
 </td>
 <td>
-  {% if item.valid_until %}
+  {% if item.expires_on %}
     {% if item.is_expired %}
       <span class="text-muted tooltip-top" title="{% trans "This ban has expired." %}">
-        {{ item.valid_until|date }}
+        {{ item.expires_on|date }}
         <span class="fa fa-exclamation text-danger"></span>
       </span>
     {% else %}
-      {{ item.valid_until|date }}
+      <span class="tooltip-top" title="{{ item.expires_on|date:"DATETIME_FORMAT" }}">
+        {{ item.formatted_expiration_date }}
+      </span>
     {% endif %}
   {% else %}
-  <em>{% trans "permanent" %}</em>
+  <em>{% trans "Never" %}</em>
   {% endif %}
 </td>
 {% for action in extra_actions %}
@@ -100,7 +102,7 @@
 {% block modal-body %}
 <div class="row">
   <div class="col-md-6">
-    {% form_row search_form.test %}
+    {% form_row search_form.check_type %}
   </div>
   <div class="col-md-6">
     {% form_row search_form.value %}

+ 1 - 1
misago/templates/misago/admin/users/ban.html

@@ -39,7 +39,7 @@ class="form-horizontal"
   <fieldset>
     <legend>{% trans "Ban settings" %}</legend>
 
-    {% form_row form.valid_until label_class field_class %}
+    {% form_row form.expires_on label_class field_class %}
 
   </fieldset>
   <fieldset>

+ 1 - 1
misago/templates/misago/modusers/ban.html

@@ -31,7 +31,7 @@
 
         {% include "misago/form_errors.html" %}
         <div class="form-body no-fieldsets">
-          {% form_row form.valid_until "col-md-3" "col-md-9" %}
+          {% form_row form.expires_on "col-md-3" "col-md-9" %}
           {% form_row form.user_message "col-md-3" "col-md-9" %}
           {% form_row form.staff_message "col-md-3" "col-md-9" %}
         </div>

+ 3 - 3
misago/templates/misago/profile/ban_details.html

@@ -6,9 +6,9 @@
 <div>
   <p class="lead pull-left">
     <span class="fa fa-lock"></span>
-    {% if ban.valid_until %}
-    {% blocktrans trimmed with user=profile banned_until=ban.valid_until %}
-    {{ user }} is banned until after {{ banned_until }}.
+    {% if ban.expires_on %}
+    {% blocktrans trimmed with user=profile banned_until=ban.formatted_expiration_date %}
+    {{ user }} is banned until {{ banned_until }}.
     {% endblocktrans %}
     {% else %}
     {% blocktrans trimmed with user=profile %}

+ 1 - 1
misago/templates/misago/user_state.html

@@ -2,7 +2,7 @@
 {% if state.is_banned %}
   {% capture trimmed as state_name %}
   {% if state.banned_until %}
-  {% blocktrans trimmed with ban_date=state.banned_until|date %}
+  {% blocktrans trimmed with ban_date=state.formatted_ban_expiration_date %}
     Banned until {{ ban_date }}
   {% endblocktrans %}
   {% else %}

+ 26 - 25
misago/users/bans.py

@@ -1,16 +1,17 @@
 
 """
-API for testing values for bans
+API for checking values for bans
 
 Calling this instead of Ban.objects.find_ban is preffered, if you don't want
 to use validate_X_banned validators
 """
-from datetime import date, datetime, timedelta
+from datetime import timedelta
 
 from django.utils import timezone
+from django.utils.dateparse import parse_datetime
 
 from misago.core import cachebuster
-from misago.users.models import BAN_IP, Ban, BanCache
+from misago.users.models import BAN_IP, Ban, BanCache, format_expiration_date
 
 
 BAN_CACHE_SESSION_KEY = 'misago_ip_check'
@@ -19,21 +20,21 @@ BAN_VERSION_KEY = 'misago_bans'
 
 def get_username_ban(username):
     try:
-        return Ban.objects.find_ban(username=username)
+        return Ban.objects.get_username_ban(username)
     except Ban.DoesNotExist:
         return None
 
 
 def get_email_ban(email):
     try:
-        return Ban.objects.find_ban(email=email)
+        return Ban.objects.get_email_ban(email)
     except Ban.DoesNotExist:
         return None
 
 
 def get_ip_ban(ip):
     try:
-        return Ban.objects.find_ban(ip=ip)
+        return Ban.objects.get_ip_ban(ip)
     except Ban.DoesNotExist:
         return None
 
@@ -65,15 +66,15 @@ def _set_user_ban_cache(user):
     ban_cache.bans_version = cachebuster.get_version(BAN_VERSION_KEY)
 
     try:
-        user_ban = Ban.objects.find_ban(username=user.username,
-                                        email=user.email)
+        user_ban = Ban.objects.get_ban(username=user.username,
+                                       email=user.email)
         ban_cache.ban = user_ban
-        ban_cache.valid_until = user_ban.valid_until
+        ban_cache.expires_on = user_ban.expires_on
         ban_cache.user_message = user_ban.user_message
         ban_cache.staff_message = user_ban.staff_message
     except Ban.DoesNotExist:
         ban_cache.ban = None
-        ban_cache.valid_until = None
+        ban_cache.expires_on = None
         ban_cache.user_message = None
         ban_cache.staff_message = None
 
@@ -103,11 +104,10 @@ def get_request_ip_ban(request):
     }
 
     if found_ban:
-        if found_ban.valid_until:
-            valid_until_as_string = found_ban.valid_until.strftime('%Y-%m-%d')
-            ban_cache['valid_until'] = valid_until_as_string
+        if found_ban.expires_on:
+            ban_cache['expires_on'] = found_ban.expires_on.isoformat()
         else:
-            ban_cache['valid_until'] = None
+            ban_cache['expires_on'] = None
 
         ban_cache.update({
                 'is_banned': True,
@@ -129,11 +129,11 @@ def _get_session_bancache(request):
             return None
         if not cachebuster.is_valid(BAN_VERSION_KEY, ban_cache['version']):
             return None
-        if ban_cache.get('valid_until'):
+        if ban_cache.get('expires_on'):
             """
-            Make two timezone unaware dates and compare them
+            Hydrate ban date
             """
-            if ban_cache.get('valid_until') < date.today():
+            if ban_cache['expires_on'] < timezone.today():
                 return None
         return ban_cache
     except KeyError:
@@ -143,10 +143,11 @@ def _get_session_bancache(request):
 def _hydrate_session_cache(ban_cache):
     hydrated = ban_cache.copy()
 
-    if hydrated.get('valid_until'):
-        expiration_datetime = datetime.strptime(ban_cache.get('valid_until'),
-                                                '%Y-%m-%d')
-        hydrated['valid_until'] = expiration_datetime.date()
+    hydrated['formatted_expiration_date'] = None
+    if hydrated.get('expires_on'):
+        hydrated['expires_on'] = parse_datetime(hydrated['expires_on'])
+        hydrated['formatted_expiration_date'] = format_expiration_date(
+            hydrated['expires_on'])
 
     return hydrated
 
@@ -156,15 +157,15 @@ Utility for banning naughty IPs
 """
 def ban_ip(ip, user_message=None, staff_message=None, length=None):
     if length:
-        valid_until = (timezone.now() + timedelta(days=length)).date()
+        expires_on = timezone.now() + timedelta(**length)
     else:
-        valid_until = None
+        expires_on = None
 
     Ban.objects.create(
-        test=BAN_IP,
+        check_type=BAN_IP,
         banned_value=ip,
         user_message=user_message,
         staff_message=staff_message,
-        valid_until=valid_until
+        expires_on=expires_on
     )
     Ban.objects.invalidate_cache()

+ 4 - 4
misago/users/decorators.py

@@ -1,5 +1,4 @@
 from django.core.exceptions import PermissionDenied
-from django.template.defaultfilters import date as format_date
 from django.utils.translation import gettext_lazy as _
 
 from misago.users.bans import get_request_ip_ban
@@ -31,9 +30,10 @@ def deny_banned_ips(f):
         if ban:
             default_message = _("Your IP address has been banned.")
             ban_message = ban.get('message') or default_message
-            if ban.get('valid_until'):
-                ban_expires = format_date(ban['valid_until'])
-                expiration_message = _("This ban will expire on %(date)s.")
+
+            if ban.get('expires'):
+                ban_expires = ban['formatted_expiration_date']
+                expiration_message = _("This ban will end on %(date)s.")
                 expiration_message = expiration_message % {'date': ban_expires}
                 ban_message = '%s\n\n%s' % (ban_message, expiration_message)
             raise PermissionDenied(ban_message)

+ 24 - 28
misago/users/forms/admin.py

@@ -273,11 +273,9 @@ class BanUsersForm(forms.Form):
         error_messages={
             'max_length': _("Message can't be longer than 1000 characters.")
         })
-    valid_until = forms.DateField(
-        label=_("Expires after"),
-        required=False, input_formats=['%m-%d-%Y'],
-        widget=forms.DateInput(
-            format='%m-%d-%Y', attrs={'data-date-format': 'MM-DD-YYYY'}),
+    expires_on = forms.DateTimeField(
+        label=_("Expires on"),
+        required=False, localize=True,
         help_text=_('Leave this field empty for this ban to never expire.'))
 
     def clean_banned_value(self):
@@ -351,8 +349,8 @@ class RankForm(forms.ModelForm):
 Bans
 """
 class BanForm(forms.ModelForm):
-    test = forms.TypedChoiceField(
-        label=_("Ban type"),
+    check_type = forms.TypedChoiceField(
+        label=_("Check type"),
         coerce=int,
         choices=BANS_CHOICES)
     banned_value = forms.CharField(
@@ -378,21 +376,19 @@ class BanForm(forms.ModelForm):
         error_messages={
             'max_length': _("Message can't be longer than 1000 characters.")
         })
-    valid_until = forms.DateField(
-        label=_("Expiration date"),
-        required=False, input_formats=['%m-%d-%Y'],
-        widget=forms.DateInput(
-            format='%m-%d-%Y', attrs={'data-date-format': 'MM-DD-YYYY'}),
+    expires_on = forms.DateTimeField(
+        label=_("Expires on"),
+        required=False, localize=True,
         help_text=_('Leave this field empty for this ban to never expire.'))
 
     class Meta:
         model = Ban
         fields = [
-            'test',
+            'check_type',
             'banned_value',
             'user_message',
             'staff_message',
-            'valid_until',
+            'expires_on',
         ]
 
     def clean_banned_value(self):
@@ -415,7 +411,7 @@ SARCH_BANS_CHOICES = (
 
 
 class SearchBansForm(forms.Form):
-    test = forms.ChoiceField(
+    check_type = forms.ChoiceField(
         label=_("Type"), required=False,
         choices=SARCH_BANS_CHOICES)
     value = forms.CharField(
@@ -424,31 +420,31 @@ class SearchBansForm(forms.Form):
     state = forms.ChoiceField(
         label=_("State"), required=False,
         choices=(
-            ('', _('All states')),
-            ('valid', _('Valid bans')),
-            ('expired', _('Expired bans')),
+            ('', _('Is used in checks')),
+            ('used', _('Yes')),
+            ('unused', _('No')),
         ))
 
     def filter_queryset(self, search_criteria, queryset):
         criteria = search_criteria
-        if criteria.get('test') == 'names':
-            queryset = queryset.filter(test=0)
+        if criteria.get('check_type') == 'names':
+            queryset = queryset.filter(check_type=0)
 
-        if criteria.get('test') == 'emails':
-            queryset = queryset.filter(test=1)
+        if criteria.get('check_type') == 'emails':
+            queryset = queryset.filter(check_type=1)
 
-        if criteria.get('test') == 'ips':
-            queryset = queryset.filter(test=2)
+        if criteria.get('check_type') == 'ips':
+            queryset = queryset.filter(check_type=2)
 
         if criteria.get('value'):
             queryset = queryset.filter(
                 banned_value__startswith=criteria.get('value').lower())
 
-        if criteria.get('state') == 'valid':
-            queryset = queryset.filter(is_valid=True)
+        if criteria.get('state') == 'used':
+            queryset = queryset.filter(is_checked=True)
 
-        if criteria.get('state') == 'expired':
-            queryset = queryset.filter(is_valid=False)
+        if criteria.get('state') == 'unused':
+            queryset = queryset.filter(is_checked=False)
 
         return queryset
 

+ 2 - 3
misago/users/forms/auth.py

@@ -2,7 +2,6 @@ from django.core.exceptions import ValidationError
 from django.contrib.auth import authenticate, get_user_model
 from django.contrib.auth.forms import (AuthenticationForm as
                                        BaseAuthenticationForm)
-from django.template.defaultfilters import date as format_date
 from django.utils.translation import ugettext_lazy as _
 
 from misago.core import forms
@@ -36,14 +35,14 @@ class MisagoAuthMixin(object):
     def confirm_user_not_banned(self, user):
         self.user_ban = get_user_ban(user)
         if self.user_ban:
-            if self.user_ban.valid_until:
+            if self.user_ban.expires_on:
                 if self.user_ban.user_message:
                     message = _("%(user)s, your account is "
                                 "banned until %(date)s for:")
                 else:
                     message = _("%(user)s, your account "
                                 "is banned until %(date)s.")
-                date_format = {'date': format_date(self.user_ban.valid_until)}
+                date_format = {'date': self.user_ban.formatted_expiration_date}
                 message = message % date_format
             else:
                 if self.user_ban.user_message:

+ 4 - 4
misago/users/forms/modusers.py

@@ -109,10 +109,10 @@ class BanForm(BanUsersForm):
                 "Required. Can't be longer than %(days)s days.",
                 self.user.acl_['max_ban_length'])
             message = message % {'days': self.user.acl_['max_ban_length']}
-            self['valid_until'].field.help_text = message
+            self['expires_on'].field.help_text = message
 
-    def clean_valid_until(self):
-        data = self.cleaned_data['valid_until']
+    def clean_expires_on(self):
+        data = self.cleaned_data['expires_on']
 
         if self.user.acl_['max_ban_length']:
             max_ban_length = timedelta(days=self.user.acl_['max_ban_length'])
@@ -132,7 +132,7 @@ class BanForm(BanUsersForm):
         new_ban = Ban(banned_value=self.user.username,
                       user_message=self.cleaned_data['user_message'],
                       staff_message=self.cleaned_data['staff_message'],
-                      valid_until=self.cleaned_data['valid_until'])
+                      expires_on=self.cleaned_data['expires_on'])
         new_ban.save()
 
         Ban.objects.invalidate_cache()

+ 4 - 5
misago/users/management/commands/bansmaintenance.py

@@ -14,15 +14,14 @@ class Command(BaseCommand):
         self.handle_bans_caches()
 
     def handle_expired_bans(self):
-        queryset = Ban.objects.filter(is_valid=True, valid_until__isnull=False)
-        queryset = queryset.filter(valid_until__lte=timezone.now().date())
+        queryset = Ban.objects.filter(is_checked=True)
+        queryset = queryset.filter(expires_on__lt=timezone.now())
 
-        expired_count = queryset.update(is_valid=False)
+        expired_count = queryset.update(is_checked=False)
         self.stdout.write('Bans invalidated: %s' % expired_count)
 
     def handle_bans_caches(self):
-        queryset = BanCache.objects.filter(valid_until__isnull=False)
-        queryset = queryset.filter(valid_until__lte=timezone.now().date())
+        queryset = BanCache.objects.filter(expires_on__lt=timezone.now())
 
         expired_count = queryset.count()
         queryset.delete()

+ 4 - 4
misago/users/migrations/0001_initial.py

@@ -154,12 +154,12 @@ class Migration(migrations.Migration):
             name='Ban',
             fields=[
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
-                ('test', models.PositiveIntegerField(default=0, db_index=True)),
+                ('check_type', models.PositiveIntegerField(default=0, db_index=True)),
                 ('banned_value', models.CharField(max_length=255, db_index=True)),
                 ('user_message', models.TextField(null=True, blank=True)),
                 ('staff_message', models.TextField(null=True, blank=True)),
-                ('valid_until', models.DateField(null=True, blank=True, db_index=True)),
-                ('is_valid', models.BooleanField(default=True, db_index=True)),
+                ('expires_on', models.DateTimeField(null=True, blank=True, db_index=True)),
+                ('is_checked', models.BooleanField(default=True, db_index=True)),
             ],
             bases=(models.Model,),
         ),
@@ -169,7 +169,7 @@ class Migration(migrations.Migration):
                 ('user_message', models.TextField(null=True, blank=True)),
                 ('staff_message', models.TextField(null=True, blank=True)),
                 ('bans_version', models.PositiveIntegerField(default=0)),
-                ('valid_until', models.DateField(null=True, blank=True)),
+                ('expires_on', models.DateTimeField(null=True, blank=True)),
                 ('ban', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to='misago_users.Ban', null=True)),
                 ('user', models.OneToOneField(related_name='ban_cache', primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
             ],

+ 67 - 38
misago/users/models/ban.py

@@ -1,16 +1,17 @@
-from datetime import timedelta
 import re
 
 from django.conf import settings
 from django.db import models
 from django.utils import timezone
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext_lazy as _, ungettext, pgettext
 
 from misago.core import cachebuster
+from misago.core.utils import date_format
 
 
 __all__ = [
-    'BAN_USERNAME', 'BAN_EMAIL', 'BAN_IP', 'BANS_CHOICES', 'Ban', 'BanCache'
+    'BAN_USERNAME', 'BAN_EMAIL', 'BAN_IP', 'BANS_CHOICES',
+    'Ban', 'BanCache', 'format_expiration_date'
 ]
 
 
@@ -29,68 +30,89 @@ BANS_CHOICES = (
 )
 
 
+def format_expiration_date(expiration_date):
+    if not expiration_date:
+        return _("Never")
+
+    now = timezone.now()
+    diff = (expiration_date - now).total_seconds()
+
+    if diff and diff < (3600 * 24):
+        format = pgettext("ban expiration hour minute",
+                          "h:i a")
+    elif now.year == expiration_date.year:
+        format = pgettext("ban expiration hour minute day month",
+                          "jS F, h:i a")
+    else:
+        format = pgettext("ban expiration hour minute day month year",
+                          "jS F Y, h:i a")
+
+    return date_format(expiration_date, format)
+
+
 class BansManager(models.Manager):
-    def is_ip_banned(self, ip):
-        return self.check_ban(ip=ip)
+    def get_ip_ban(self, ip):
+        return self.get_ban(ip=ip)
 
-    def is_username_banned(self, username):
-        return self.check_ban(username=username)
+    def get_username_ban(self, username):
+        return self.get_ban(username=username)
 
-    def is_email_banned(self, email):
-        return self.check_ban(email=email)
+    def get_email_ban(self, email):
+        return self.get_ban(email=email)
 
     def invalidate_cache(self):
         cachebuster.invalidate(BAN_CACHEBUSTER)
 
-    def find_ban(self, username=None, email=None, ip=None):
-        tests = []
+    def get_ban(self, username=None, email=None, ip=None):
+        checks = []
 
         if username:
             username = username.lower()
-            tests.append(BAN_USERNAME)
+            checks.append(BAN_USERNAME)
         if email:
             email = email.lower()
-            tests.append(BAN_EMAIL)
+            checks.append(BAN_EMAIL)
         if ip:
-            tests.append(BAN_IP)
+            checks.append(BAN_IP)
 
-        queryset = self.filter(is_valid=True)
-        if len(tests) == 1:
-            queryset = queryset.filter(test=tests[0])
-        elif tests:
-            queryset = queryset.filter(test__in=tests)
+        queryset = self.filter(is_checked=True)
+        if len(checks) == 1:
+            queryset = queryset.filter(check_type=checks[0])
+        elif checks:
+            queryset = queryset.filter(check_type__in=checks)
 
         for ban in queryset.order_by('-id').iterator():
-            if (ban.test == BAN_USERNAME and username and
-                    ban.test_value(username)):
+            if (ban.check_type == BAN_USERNAME and username and
+                    ban.check_value(username)):
                 return ban
-            elif ban.test == BAN_EMAIL and email and ban.test_value(email):
+            elif (ban.check_type == BAN_EMAIL and email and
+                    ban.check_value(email)):
                 return ban
-            elif ban.test == BAN_IP and ip and ban.test_value(ip):
+            elif ban.check_type == BAN_IP and ip and ban.check_value(ip):
                 return ban
         else:
-            raise Ban.DoesNotExist('no valid ban for values has been found')
+            raise Ban.DoesNotExist('specified values are not banned')
 
 
 class Ban(models.Model):
-    test = models.PositiveIntegerField(default=BAN_USERNAME, db_index=True)
+    check_type = models.PositiveIntegerField(default=BAN_USERNAME, db_index=True)
     banned_value = models.CharField(max_length=255, db_index=True)
     user_message = models.TextField(null=True, blank=True)
     staff_message = models.TextField(null=True, blank=True)
-    valid_until = models.DateField(null=True, blank=True, db_index=True)
-    is_valid = models.BooleanField(default=True, db_index=True)
+    expires_on = models.DateTimeField(null=True, blank=True, db_index=True)
+    is_checked = models.BooleanField(default=True, db_index=True)
 
     objects = BansManager()
 
     def save(self, *args, **kwargs):
         self.banned_value = self.banned_value.lower()
-        self.is_valid = not self.is_expired
+        self.is_checked = not self.is_expired
 
         return super(Ban, self).save(*args, **kwargs)
 
     @property
-    def test_name(self):
-        return BANS_CHOICES[self.test][1]
+    def check_name(self):
+        return BANS_CHOICES[self.check_type][1]
 
     @property
     def name(self):
@@ -98,12 +120,16 @@ class Ban(models.Model):
 
     @property
     def is_expired(self):
-        if self.valid_until:
-            return self.valid_until < timezone.now().date()
+        if self.expires_on:
+            return self.expires_on < timezone.now()
         else:
             return False
 
-    def test_value(self, value):
+    @property
+    def formatted_expiration_date(self):
+        return format_expiration_date(self.expires_on)
+
+    def check_value(self, value):
         if '*' in self.banned_value:
             regex = re.escape(self.banned_value).replace('\*', '(.*?)')
             return re.search('^%s$' % regex, value) is not None
@@ -111,7 +137,7 @@ class Ban(models.Model):
             return self.banned_value == value
 
     def lift(self):
-        self.valid_until = (timezone.now() - timedelta(days=1)).date()
+        self.expires_on = timezone.now()
 
 
 class BanCache(models.Model):
@@ -122,7 +148,11 @@ class BanCache(models.Model):
     bans_version = models.PositiveIntegerField(default=0)
     user_message = models.TextField(null=True, blank=True)
     staff_message = models.TextField(null=True, blank=True)
-    valid_until = models.DateField(null=True, blank=True)
+    expires_on = models.DateTimeField(null=True, blank=True)
+
+    @property
+    def formatted_expiration_date(self):
+        return format_expiration_date(self.expires_on)
 
     @property
     def is_banned(self):
@@ -132,7 +162,6 @@ class BanCache(models.Model):
     def is_valid(self):
         version_is_valid = cachebuster.is_valid(BAN_CACHEBUSTER,
                                                 self.bans_version)
-        date_today = timezone.now().date()
-        not_expired = not self.valid_until or self.valid_until > date_today
+        expired = self.expires_on and self.expires_on < timezone.now()
 
-        return version_is_valid and not_expired
+        return version_is_valid and not expired

+ 4 - 1
misago/users/online/utils.py

@@ -35,7 +35,10 @@ def get_user_state(user, acl):
     user_ban = get_user_ban(user)
     if user_ban:
         user_state['is_banned'] = True
-        user_state['banned_until'] = user_ban.valid_until
+        user_state['banned_until'] = user_ban.expires_on
+
+        ban_expiration_date = user_ban.formatted_expiration_date
+        user_state['formatted_ban_expiration_date'] = ban_expiration_date
 
     try:
         if not user.is_hiding_presence or acl['can_see_hidden_users']:

+ 4 - 2
misago/users/tests/test_activation_views.py

@@ -32,7 +32,8 @@ class ActivationViewsTests(TestCase):
         User = get_user_model()
         User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
                                  requires_activation=1)
-        Ban.objects.create(test=BAN_USERNAME, banned_value='bob',
+        Ban.objects.create(check_type=BAN_USERNAME,
+                           banned_value='bob',
                            user_message='Nope!')
 
         response = self.client.post(
@@ -61,7 +62,8 @@ class ActivationViewsTests(TestCase):
         User = get_user_model()
         test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
                                              requires_activation=1)
-        Ban.objects.create(test=BAN_USERNAME, banned_value='bob',
+        Ban.objects.create(check_type=BAN_USERNAME,
+                           banned_value='bob',
                            user_message='Nope!')
 
         activation_token = make_activation_token(test_user)

+ 2 - 1
misago/users/tests/test_auth_views.py

@@ -39,7 +39,8 @@ class LoginViewTests(TestCase):
         """login view fails to sign banned user in"""
         User = get_user_model()
         User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
-        Ban.objects.create(test=BAN_USERNAME, banned_value='bob',
+        Ban.objects.create(check_type=BAN_USERNAME,
+                           banned_value='bob',
                            user_message='Nope!')
 
         response = self.client.post(

+ 31 - 31
misago/users/tests/test_ban_model.py

@@ -6,64 +6,64 @@ from misago.users.models import Ban, BAN_USERNAME, BAN_EMAIL, BAN_IP
 class BansManagerTests(TestCase):
     def setUp(self):
         Ban.objects.bulk_create([
-            Ban(test=BAN_USERNAME, banned_value='bob'),
-            Ban(test=BAN_EMAIL, banned_value='bob@test.com'),
-            Ban(test=BAN_IP, banned_value='127.0.0.1'),
+            Ban(check_type=BAN_USERNAME, banned_value='bob'),
+            Ban(check_type=BAN_EMAIL, banned_value='bob@test.com'),
+            Ban(check_type=BAN_IP, banned_value='127.0.0.1'),
         ])
 
-    def test_find_ban_for_banned_name(self):
-        """find_ban finds ban for given username"""
-        self.assertIsNotNone(Ban.objects.find_ban(username='Bob'))
+    def test_get_ban_for_banned_name(self):
+        """get_ban finds ban for given username"""
+        self.assertIsNotNone(Ban.objects.get_ban(username='Bob'))
         with self.assertRaises(Ban.DoesNotExist):
-            Ban.objects.find_ban(username='Jeb')
+            Ban.objects.get_ban(username='Jeb')
 
-    def test_find_ban_for_banned_email(self):
-        """find_ban finds ban for given email"""
-        self.assertIsNotNone(Ban.objects.find_ban(email='bob@test.com'))
+    def test_get_ban_for_banned_email(self):
+        """get_ban finds ban for given email"""
+        self.assertIsNotNone(Ban.objects.get_ban(email='bob@test.com'))
         with self.assertRaises(Ban.DoesNotExist):
-            Ban.objects.find_ban(email='jeb@test.com')
+            Ban.objects.get_ban(email='jeb@test.com')
 
-    def test_find_ban_for_banned_ip(self):
-        """find_ban finds ban for given ip"""
-        self.assertIsNotNone(Ban.objects.find_ban(ip='127.0.0.1'))
+    def test_get_ban_for_banned_ip(self):
+        """get_ban finds ban for given ip"""
+        self.assertIsNotNone(Ban.objects.get_ban(ip='127.0.0.1'))
         with self.assertRaises(Ban.DoesNotExist):
-            Ban.objects.find_ban(ip='42.0.0.1')
+            Ban.objects.get_ban(ip='42.0.0.1')
 
-    def test_find_ban_for_all_bans(self):
-        """find_ban finds ban for given values"""
+    def test_get_ban_for_all_bans(self):
+        """get_ban finds ban for given values"""
         valid_kwargs = {'username': 'bob', 'ip': '42.51.52.51'}
-        self.assertIsNotNone(Ban.objects.find_ban(**valid_kwargs))
+        self.assertIsNotNone(Ban.objects.get_ban(**valid_kwargs))
 
         invalid_kwargs = {'username': 'bsob', 'ip': '42.51.52.51'}
         with self.assertRaises(Ban.DoesNotExist):
-            Ban.objects.find_ban(**invalid_kwargs)
+            Ban.objects.get_ban(**invalid_kwargs)
 
 
 class BanTests(TestCase):
-    def test_test_value_literal(self):
+    def test_check_value_literal(self):
         """ban correctly tests given values"""
         test_ban = Ban(banned_value='bob')
 
-        self.assertTrue(test_ban.test_value('bob'))
-        self.assertFalse(test_ban.test_value('bobby'))
+        self.assertTrue(test_ban.check_value('bob'))
+        self.assertFalse(test_ban.check_value('bobby'))
 
-    def test_test_value_starts_with(self):
+    def test_check_value_starts_with(self):
         """ban correctly tests given values"""
         test_ban = Ban(banned_value='bob*')
 
-        self.assertTrue(test_ban.test_value('bob'))
-        self.assertTrue(test_ban.test_value('bobby'))
+        self.assertTrue(test_ban.check_value('bob'))
+        self.assertTrue(test_ban.check_value('bobby'))
 
-    def test_test_value_middle_match(self):
+    def test_check_value_middle_match(self):
         """ban correctly tests given values"""
         test_ban = Ban(banned_value='b*b')
 
-        self.assertTrue(test_ban.test_value('bob'))
-        self.assertFalse(test_ban.test_value('bobby'))
+        self.assertTrue(test_ban.check_value('bob'))
+        self.assertFalse(test_ban.check_value('bobby'))
 
-    def test_test_value_ends_witch(self):
+    def test_check_value_ends_witch(self):
         """ban correctly tests given values"""
         test_ban = Ban(banned_value='*bob')
 
-        self.assertTrue(test_ban.test_value('lebob'))
-        self.assertFalse(test_ban.test_value('bobby'))
+        self.assertTrue(test_ban.check_value('lebob'))
+        self.assertFalse(test_ban.check_value('bobby'))

+ 10 - 9
misago/users/tests/test_banadmin_views.py

@@ -25,17 +25,17 @@ class BanAdminViewsTests(AdminTestCase):
         response = self.client.get(response['location'])
         self.assertEqual(response.status_code, 200)
 
-    def test_mass_activation(self):
+    def test_mass_delete(self):
         """adminview deletes multiple bans"""
         for i in xrange(10):
             response = self.client.post(
                 reverse('misago:admin:users:bans:new'),
                 data={
-                    'test': '1',
+                    'check_type': '1',
                     'banned_value': 'test@test.com',
                     'user_message': 'Lorem ipsum dolor met',
                     'staff_message': 'Sit amet elit',
-                    'valid_until': '12-24-%s' % unicode(date.today().year + 1),
+                    'expires_on': '%s-12-24' % unicode(date.today().year + 1),
                 })
 
         self.assertEqual(Ban.objects.count(), 10)
@@ -59,11 +59,11 @@ class BanAdminViewsTests(AdminTestCase):
         response = self.client.post(
             reverse('misago:admin:users:bans:new'),
             data={
-                'test': '1',
+                'check_type': '1',
                 'banned_value': 'test@test.com',
                 'user_message': 'Lorem ipsum dolor met',
                 'staff_message': 'Sit amet elit',
-                'valid_until': '12-24-%s' % unicode(date.today().year + 1),
+                'expires_on': '%s-12-24' % unicode(date.today().year + 1),
             })
         self.assertEqual(response.status_code, 302)
 
@@ -77,7 +77,7 @@ class BanAdminViewsTests(AdminTestCase):
         self.client.post(
             reverse('misago:admin:users:bans:new'),
             data={
-                'test': '0',
+                'check_type': '0',
                 'banned_value': 'Admin',
             })
 
@@ -86,11 +86,11 @@ class BanAdminViewsTests(AdminTestCase):
             reverse('misago:admin:users:bans:edit',
                     kwargs={'ban_id': test_ban.pk}),
             data={
-                'test': '1',
+                'check_type': '1',
                 'banned_value': 'test@test.com',
                 'user_message': 'Lorem ipsum dolor met',
                 'staff_message': 'Sit amet elit',
-                'valid_until': '12-24-%s' % unicode(date.today().year + 1),
+                'expires_on': '%s-12-24' % unicode(date.today().year + 1),
             })
         self.assertEqual(response.status_code, 302)
 
@@ -98,13 +98,14 @@ class BanAdminViewsTests(AdminTestCase):
         response = self.client.get(response['location'])
         self.assertEqual(response.status_code, 200)
         self.assertIn('test@test.com', response.content)
+        #raise Exception('FIX WARNING!')
 
     def test_delete_view(self):
         """delete ban view has no showstoppers"""
         self.client.post(
             reverse('misago:admin:users:bans:new'),
             data={
-                'test': '0',
+                'check_type': '0',
                 'banned_value': 'TestBan',
             })
 

+ 18 - 8
misago/users/tests/test_bans.py

@@ -1,7 +1,8 @@
-from datetime import date, timedelta
+from datetime import timedelta
 
 from django.contrib.auth import get_user_model
 from django.test import TestCase
+from django.utils import timezone
 
 from misago.users.bans import get_user_ban, get_request_ip_ban
 from misago.users.models import Ban, BAN_IP
@@ -36,7 +37,7 @@ class UserBansTests(TestCase):
         Ban.objects.create(banned_value='bo*',
                            user_message='User reason',
                            staff_message='Staff reason',
-                           valid_until=date.today() + timedelta(days=7))
+                           expires_on=timezone.now() + timedelta(days=7))
 
         user_ban = get_user_ban(self.user)
         self.assertIsNotNone(user_ban)
@@ -47,7 +48,7 @@ class UserBansTests(TestCase):
     def test_expired_ban(self):
         """user is not caught by expired ban"""
         Ban.objects.create(banned_value='bo*',
-                           valid_until=date.today() - timedelta(days=7))
+                           expires_on=timezone.now() - timedelta(days=7))
 
         self.assertIsNone(get_user_ban(self.user))
         self.assertFalse(self.user.ban_cache.is_banned)
@@ -67,7 +68,7 @@ class RequestIPBansTests(TestCase):
 
     def test_permanent_ban(self):
         """ip is caught by permanent ban"""
-        Ban.objects.create(test=BAN_IP,
+        Ban.objects.create(check_type=BAN_IP,
                            banned_value='127.0.0.1',
                            user_message='User reason')
 
@@ -76,24 +77,33 @@ class RequestIPBansTests(TestCase):
         self.assertEqual(ip_ban['ip'], '127.0.0.1')
         self.assertEqual(ip_ban['message'], 'User reason')
 
+        # repeated call uses cache
+        get_request_ip_ban(FakeRequest())
+
     def test_temporary_ban(self):
         """ip is caught by temporary ban"""
-        Ban.objects.create(test=BAN_IP,
+        Ban.objects.create(check_type=BAN_IP,
                            banned_value='127.0.0.1',
                            user_message='User reason',
-                           valid_until=date.today() + timedelta(days=7))
+                           expires_on=timezone.now() + timedelta(days=7))
 
         ip_ban = get_request_ip_ban(FakeRequest())
         self.assertTrue(ip_ban['is_banned'])
         self.assertEqual(ip_ban['ip'], '127.0.0.1')
         self.assertEqual(ip_ban['message'], 'User reason')
 
+        # repeated call uses cache
+        get_request_ip_ban(FakeRequest())
+
     def test_expired_ban(self):
         """ip is not caught by expired ban"""
-        Ban.objects.create(test=BAN_IP,
+        Ban.objects.create(check_type=BAN_IP,
                            banned_value='127.0.0.1',
                            user_message='User reason',
-                           valid_until=date.today() - timedelta(days=7))
+                           expires_on=timezone.now() - timedelta(days=7))
 
         ip_ban = get_request_ip_ban(FakeRequest())
         self.assertIsNone(ip_ban)
+
+        # repeated call uses cache
+        get_request_ip_ban(FakeRequest())

+ 10 - 10
misago/users/tests/test_bansmaintenance.py

@@ -16,10 +16,10 @@ class BansMaintenanceTests(TestCase):
         # create 5 bans then update their valid date to past one
         for i in xrange(5):
             Ban.objects.create(banned_value="abcd")
-        bans_expired = (timezone.now() - timedelta(days=10)).date()
-        Ban.objects.all().update(valid_until=bans_expired, is_valid=True)
+        expired_date = (timezone.now() - timedelta(days=10))
+        Ban.objects.all().update(expires_on=expired_date, is_checked=True)
 
-        self.assertEqual(Ban.objects.filter(is_valid=True).count(), 5)
+        self.assertEqual(Ban.objects.filter(is_checked=True).count(), 5)
 
         command = bansmaintenance.Command()
 
@@ -29,7 +29,7 @@ class BansMaintenanceTests(TestCase):
 
         self.assertEqual(command_output, 'Bans invalidated: 5')
 
-        self.assertEqual(Ban.objects.filter(is_valid=True).count(), 0)
+        self.assertEqual(Ban.objects.filter(is_checked=True).count(), 0)
 
     def test_bans_caches_updates(self):
         """ban caches are updated"""
@@ -42,7 +42,7 @@ class BansMaintenanceTests(TestCase):
         user_ban = bans.get_user_ban(user)
 
         self.assertIsNotNone(user_ban)
-        self.assertEqual(Ban.objects.filter(is_valid=True).count(), 1)
+        self.assertEqual(Ban.objects.filter(is_checked=True).count(), 1)
 
         # first call didn't touch ban
         command = bansmaintenance.Command()
@@ -52,12 +52,12 @@ class BansMaintenanceTests(TestCase):
         command_output = out.getvalue().splitlines()[1].strip()
 
         self.assertEqual(command_output, 'Ban caches emptied: 0')
-        self.assertEqual(Ban.objects.filter(is_valid=True).count(), 1)
+        self.assertEqual(Ban.objects.filter(is_checked=True).count(), 1)
 
         # expire bans
-        bans_expired = (timezone.now() - timedelta(days=10)).date()
-        Ban.objects.all().update(valid_until=bans_expired, is_valid=True)
-        BanCache.objects.all().update(valid_until=bans_expired)
+        expired_date = (timezone.now() - timedelta(days=10))
+        Ban.objects.all().update(expires_on=expired_date, is_checked=True)
+        BanCache.objects.all().update(expires_on=expired_date)
 
         # invalidate expired ban cache
         out = StringIO()
@@ -65,7 +65,7 @@ class BansMaintenanceTests(TestCase):
         command_output = out.getvalue().splitlines()[1].strip()
 
         self.assertEqual(command_output, 'Ban caches emptied: 1')
-        self.assertEqual(Ban.objects.filter(is_valid=True).count(), 0)
+        self.assertEqual(Ban.objects.filter(is_checked=True).count(), 0)
 
         # see if user is banned anymore
         user = User.objects.get(id=user.id)

+ 4 - 2
misago/users/tests/test_forgottenpassword_views.py

@@ -30,7 +30,8 @@ class ForgottenPasswordViewsTests(TestCase):
         """request new password view errors for banned users"""
         User = get_user_model()
         User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
-        Ban.objects.create(test=BAN_USERNAME, banned_value='bob',
+        Ban.objects.create(check_type=BAN_USERNAME,
+                           banned_value='bob',
                            user_message='Nope!')
 
         response = self.client.post(
@@ -61,7 +62,8 @@ class ForgottenPasswordViewsTests(TestCase):
         test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
         old_password = test_user.password
 
-        Ban.objects.create(test=BAN_USERNAME, banned_value='bob',
+        Ban.objects.create(check_type=BAN_USERNAME,
+                           banned_value='bob',
                            user_message='Nope!')
 
         password_token = make_password_reset_token(test_user)

+ 2 - 6
misago/users/tests/test_validators.py

@@ -37,7 +37,7 @@ class ValidateEmailAvailableTests(TestCase):
 
 class ValidateEmailBannedTests(TestCase):
     def setUp(self):
-        Ban.objects.create(test=BAN_EMAIL, banned_value="ban@test.com")
+        Ban.objects.create(check_type=BAN_EMAIL, banned_value="ban@test.com")
 
     def test_unbanned_name(self):
         """unbanned email passes validation"""
@@ -97,7 +97,7 @@ class ValidateUsernameAvailableTests(TestCase):
 
 class ValidateUsernameBannedTests(TestCase):
     def setUp(self):
-        Ban.objects.create(test=BAN_USERNAME, banned_value="Bob")
+        Ban.objects.create(check_type=BAN_USERNAME, banned_value="Bob")
 
     def test_unbanned_name(self):
         """unbanned name passes validation"""
@@ -142,7 +142,3 @@ class ValidateUsernameLengthTests(TestCase):
             validate_username_length('a' * (settings.username_length_min - 1))
         with self.assertRaises(ValidationError):
             validate_username_length('a' * (settings.username_length_max + 1))
-
-
-class TestRegistrationValidators(TestCase):
-    pass

+ 1 - 1
misago/users/views/admin/users.py

@@ -129,7 +129,7 @@ class UsersList(UserAdmin, generic.ListView):
                         banned_value=user.username,
                         user_message=form.cleaned_data.get('user_message'),
                         staff_message=form.cleaned_data.get('staff_message'),
-                        valid_until=form.cleaned_data.get('valid_until')
+                        expires_on=form.cleaned_data.get('expires_on')
                     )
 
                 Ban.objects.invalidate_cache()

+ 3 - 1
misago/users/views/register.py

@@ -54,7 +54,9 @@ def register(request):
 
                 message_formats = {'date': date_format(timezone.now())}
                 staff_message = staff_message % message_formats
-                ban_ip(request.user.ip, staff_message=staff_message, length=1)
+                ban_ip(request.user.ip,
+                       staff_message=staff_message,
+                       length={'days': 1})
                 raise e
 
             activation_kwargs = {}