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-uiserver.js',
             'misago/js/misago-bindings.js',
             'misago/js/misago-bindings.js',
             'misago/js/misago-tooltips.js',
             'misago/js/misago-tooltips.js',
+            'misago/js/misago-datetimepicker.js',
             'misago/js/misago-yesnoswitch.js',
             'misago/js/misago-yesnoswitch.js',
             'misago/js/misago-dropdowns.js',
             'misago/js/misago-dropdowns.js',
             'misago/js/misago-modal.js',
             'misago/js/misago-modal.js',
@@ -92,6 +93,7 @@ PIPELINE_JS = {
             'misago/admin/js/bootstrap.js',
             'misago/admin/js/bootstrap.js',
             'misago/admin/js/moment.min.js',
             'misago/admin/js/moment.min.js',
             'misago/admin/js/bootstrap-datetimepicker.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-timestamps.js',
             'misago/admin/js/misago-tooltips.js',
             'misago/admin/js/misago-tooltips.js',
             'misago/admin/js/misago-tables.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 "pager.less";
 @import "tables.less";
 @import "tables.less";
 @import "lists.less";
 @import "lists.less";
+@import "datetimepicker.less";
 @import "yesnoswitch.less";
 @import "yesnoswitch.less";
 
 
 // Layout elements
 // 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 "tables.less";
 @import "threadslists.less";
 @import "threadslists.less";
 @import "typography.less";
 @import "typography.less";
+@import "datepicker.less";
 @import "yesnoswitch.less";
 @import "yesnoswitch.less";
 
 
 // Layout elements
 // Layout elements

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

@@ -12,14 +12,12 @@
 // 3rd party libs
 // 3rd party libs
 @import "font-awesome.css";
 @import "font-awesome.css";
 @import "jquery.Jcrop.css";
 @import "jquery.Jcrop.css";
+@import "bootstrap-datetimepicker.less";
 
 
 // Import other files
 // Import other files
 @import "bootstrap/bootstrap.less";
 @import "bootstrap/bootstrap.less";
 @import "misago/misago.less";
 @import "misago/misago.less";
 @import "flavor/flavor.less";
 @import "flavor/flavor.less";
 
 
-// Bootstrap 3rd party libs
-@import "bootstrap-datetimepicker.less";
-
 // Rank overrides
 // Rank overrides
 @import "ranks.less";
 @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>
                     </div>
                 {% endif %}
                 {% endif %}
             {% else %}
             {% 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 %}
                     {% crispy_field field %}
                     {% include 'bootstrap3/layout/help_text_and_errors.html' %}
                     {% include 'bootstrap3/layout/help_text_and_errors.html' %}
                 </div>
                 </div>

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

@@ -42,9 +42,9 @@ class="form-horizontal"
   <fieldset>
   <fieldset>
     <legend>{% trans "Ban settings" %}</legend>
     <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.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>
   <fieldset>
   <fieldset>

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

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

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

@@ -39,7 +39,7 @@ class="form-horizontal"
   <fieldset>
   <fieldset>
     <legend>{% trans "Ban settings" %}</legend>
     <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>
   <fieldset>
   <fieldset>

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

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

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

@@ -6,9 +6,9 @@
 <div>
 <div>
   <p class="lead pull-left">
   <p class="lead pull-left">
     <span class="fa fa-lock"></span>
     <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 %}
     {% endblocktrans %}
     {% else %}
     {% else %}
     {% blocktrans trimmed with user=profile %}
     {% blocktrans trimmed with user=profile %}

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

@@ -2,7 +2,7 @@
 {% if state.is_banned %}
 {% if state.is_banned %}
   {% capture trimmed as state_name %}
   {% capture trimmed as state_name %}
   {% if state.banned_until %}
   {% 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 }}
     Banned until {{ ban_date }}
   {% endblocktrans %}
   {% endblocktrans %}
   {% else %}
   {% 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
 Calling this instead of Ban.objects.find_ban is preffered, if you don't want
 to use validate_X_banned validators
 to use validate_X_banned validators
 """
 """
-from datetime import date, datetime, timedelta
+from datetime import timedelta
 
 
 from django.utils import timezone
 from django.utils import timezone
+from django.utils.dateparse import parse_datetime
 
 
 from misago.core import cachebuster
 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'
 BAN_CACHE_SESSION_KEY = 'misago_ip_check'
@@ -19,21 +20,21 @@ BAN_VERSION_KEY = 'misago_bans'
 
 
 def get_username_ban(username):
 def get_username_ban(username):
     try:
     try:
-        return Ban.objects.find_ban(username=username)
+        return Ban.objects.get_username_ban(username)
     except Ban.DoesNotExist:
     except Ban.DoesNotExist:
         return None
         return None
 
 
 
 
 def get_email_ban(email):
 def get_email_ban(email):
     try:
     try:
-        return Ban.objects.find_ban(email=email)
+        return Ban.objects.get_email_ban(email)
     except Ban.DoesNotExist:
     except Ban.DoesNotExist:
         return None
         return None
 
 
 
 
 def get_ip_ban(ip):
 def get_ip_ban(ip):
     try:
     try:
-        return Ban.objects.find_ban(ip=ip)
+        return Ban.objects.get_ip_ban(ip)
     except Ban.DoesNotExist:
     except Ban.DoesNotExist:
         return None
         return None
 
 
@@ -65,15 +66,15 @@ def _set_user_ban_cache(user):
     ban_cache.bans_version = cachebuster.get_version(BAN_VERSION_KEY)
     ban_cache.bans_version = cachebuster.get_version(BAN_VERSION_KEY)
 
 
     try:
     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.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.user_message = user_ban.user_message
         ban_cache.staff_message = user_ban.staff_message
         ban_cache.staff_message = user_ban.staff_message
     except Ban.DoesNotExist:
     except Ban.DoesNotExist:
         ban_cache.ban = None
         ban_cache.ban = None
-        ban_cache.valid_until = None
+        ban_cache.expires_on = None
         ban_cache.user_message = None
         ban_cache.user_message = None
         ban_cache.staff_message = None
         ban_cache.staff_message = None
 
 
@@ -103,11 +104,10 @@ def get_request_ip_ban(request):
     }
     }
 
 
     if found_ban:
     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:
         else:
-            ban_cache['valid_until'] = None
+            ban_cache['expires_on'] = None
 
 
         ban_cache.update({
         ban_cache.update({
                 'is_banned': True,
                 'is_banned': True,
@@ -129,11 +129,11 @@ def _get_session_bancache(request):
             return None
             return None
         if not cachebuster.is_valid(BAN_VERSION_KEY, ban_cache['version']):
         if not cachebuster.is_valid(BAN_VERSION_KEY, ban_cache['version']):
             return None
             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 None
         return ban_cache
         return ban_cache
     except KeyError:
     except KeyError:
@@ -143,10 +143,11 @@ def _get_session_bancache(request):
 def _hydrate_session_cache(ban_cache):
 def _hydrate_session_cache(ban_cache):
     hydrated = ban_cache.copy()
     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
     return hydrated
 
 
@@ -156,15 +157,15 @@ Utility for banning naughty IPs
 """
 """
 def ban_ip(ip, user_message=None, staff_message=None, length=None):
 def ban_ip(ip, user_message=None, staff_message=None, length=None):
     if length:
     if length:
-        valid_until = (timezone.now() + timedelta(days=length)).date()
+        expires_on = timezone.now() + timedelta(**length)
     else:
     else:
-        valid_until = None
+        expires_on = None
 
 
     Ban.objects.create(
     Ban.objects.create(
-        test=BAN_IP,
+        check_type=BAN_IP,
         banned_value=ip,
         banned_value=ip,
         user_message=user_message,
         user_message=user_message,
         staff_message=staff_message,
         staff_message=staff_message,
-        valid_until=valid_until
+        expires_on=expires_on
     )
     )
     Ban.objects.invalidate_cache()
     Ban.objects.invalidate_cache()

+ 4 - 4
misago/users/decorators.py

@@ -1,5 +1,4 @@
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
-from django.template.defaultfilters import date as format_date
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from misago.users.bans import get_request_ip_ban
 from misago.users.bans import get_request_ip_ban
@@ -31,9 +30,10 @@ def deny_banned_ips(f):
         if ban:
         if ban:
             default_message = _("Your IP address has been banned.")
             default_message = _("Your IP address has been banned.")
             ban_message = ban.get('message') or default_message
             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}
                 expiration_message = expiration_message % {'date': ban_expires}
                 ban_message = '%s\n\n%s' % (ban_message, expiration_message)
                 ban_message = '%s\n\n%s' % (ban_message, expiration_message)
             raise PermissionDenied(ban_message)
             raise PermissionDenied(ban_message)

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

@@ -273,11 +273,9 @@ class BanUsersForm(forms.Form):
         error_messages={
         error_messages={
             'max_length': _("Message can't be longer than 1000 characters.")
             '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.'))
         help_text=_('Leave this field empty for this ban to never expire.'))
 
 
     def clean_banned_value(self):
     def clean_banned_value(self):
@@ -351,8 +349,8 @@ class RankForm(forms.ModelForm):
 Bans
 Bans
 """
 """
 class BanForm(forms.ModelForm):
 class BanForm(forms.ModelForm):
-    test = forms.TypedChoiceField(
-        label=_("Ban type"),
+    check_type = forms.TypedChoiceField(
+        label=_("Check type"),
         coerce=int,
         coerce=int,
         choices=BANS_CHOICES)
         choices=BANS_CHOICES)
     banned_value = forms.CharField(
     banned_value = forms.CharField(
@@ -378,21 +376,19 @@ class BanForm(forms.ModelForm):
         error_messages={
         error_messages={
             'max_length': _("Message can't be longer than 1000 characters.")
             '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.'))
         help_text=_('Leave this field empty for this ban to never expire.'))
 
 
     class Meta:
     class Meta:
         model = Ban
         model = Ban
         fields = [
         fields = [
-            'test',
+            'check_type',
             'banned_value',
             'banned_value',
             'user_message',
             'user_message',
             'staff_message',
             'staff_message',
-            'valid_until',
+            'expires_on',
         ]
         ]
 
 
     def clean_banned_value(self):
     def clean_banned_value(self):
@@ -415,7 +411,7 @@ SARCH_BANS_CHOICES = (
 
 
 
 
 class SearchBansForm(forms.Form):
 class SearchBansForm(forms.Form):
-    test = forms.ChoiceField(
+    check_type = forms.ChoiceField(
         label=_("Type"), required=False,
         label=_("Type"), required=False,
         choices=SARCH_BANS_CHOICES)
         choices=SARCH_BANS_CHOICES)
     value = forms.CharField(
     value = forms.CharField(
@@ -424,31 +420,31 @@ class SearchBansForm(forms.Form):
     state = forms.ChoiceField(
     state = forms.ChoiceField(
         label=_("State"), required=False,
         label=_("State"), required=False,
         choices=(
         choices=(
-            ('', _('All states')),
-            ('valid', _('Valid bans')),
-            ('expired', _('Expired bans')),
+            ('', _('Is used in checks')),
+            ('used', _('Yes')),
+            ('unused', _('No')),
         ))
         ))
 
 
     def filter_queryset(self, search_criteria, queryset):
     def filter_queryset(self, search_criteria, queryset):
         criteria = search_criteria
         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'):
         if criteria.get('value'):
             queryset = queryset.filter(
             queryset = queryset.filter(
                 banned_value__startswith=criteria.get('value').lower())
                 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
         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 import authenticate, get_user_model
 from django.contrib.auth.forms import (AuthenticationForm as
 from django.contrib.auth.forms import (AuthenticationForm as
                                        BaseAuthenticationForm)
                                        BaseAuthenticationForm)
-from django.template.defaultfilters import date as format_date
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 
 from misago.core import forms
 from misago.core import forms
@@ -36,14 +35,14 @@ class MisagoAuthMixin(object):
     def confirm_user_not_banned(self, user):
     def confirm_user_not_banned(self, user):
         self.user_ban = get_user_ban(user)
         self.user_ban = get_user_ban(user)
         if self.user_ban:
         if self.user_ban:
-            if self.user_ban.valid_until:
+            if self.user_ban.expires_on:
                 if self.user_ban.user_message:
                 if self.user_ban.user_message:
                     message = _("%(user)s, your account is "
                     message = _("%(user)s, your account is "
                                 "banned until %(date)s for:")
                                 "banned until %(date)s for:")
                 else:
                 else:
                     message = _("%(user)s, your account "
                     message = _("%(user)s, your account "
                                 "is banned until %(date)s.")
                                 "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
                 message = message % date_format
             else:
             else:
                 if self.user_ban.user_message:
                 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.",
                 "Required. Can't be longer than %(days)s days.",
                 self.user.acl_['max_ban_length'])
                 self.user.acl_['max_ban_length'])
             message = message % {'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']:
         if self.user.acl_['max_ban_length']:
             max_ban_length = timedelta(days=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,
         new_ban = Ban(banned_value=self.user.username,
                       user_message=self.cleaned_data['user_message'],
                       user_message=self.cleaned_data['user_message'],
                       staff_message=self.cleaned_data['staff_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()
         new_ban.save()
 
 
         Ban.objects.invalidate_cache()
         Ban.objects.invalidate_cache()

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

@@ -14,15 +14,14 @@ class Command(BaseCommand):
         self.handle_bans_caches()
         self.handle_bans_caches()
 
 
     def handle_expired_bans(self):
     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)
         self.stdout.write('Bans invalidated: %s' % expired_count)
 
 
     def handle_bans_caches(self):
     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()
         expired_count = queryset.count()
         queryset.delete()
         queryset.delete()

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

@@ -154,12 +154,12 @@ class Migration(migrations.Migration):
             name='Ban',
             name='Ban',
             fields=[
             fields=[
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                 ('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)),
                 ('banned_value', models.CharField(max_length=255, db_index=True)),
                 ('user_message', models.TextField(null=True, blank=True)),
                 ('user_message', models.TextField(null=True, blank=True)),
                 ('staff_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,),
             bases=(models.Model,),
         ),
         ),
@@ -169,7 +169,7 @@ class Migration(migrations.Migration):
                 ('user_message', models.TextField(null=True, blank=True)),
                 ('user_message', models.TextField(null=True, blank=True)),
                 ('staff_message', models.TextField(null=True, blank=True)),
                 ('staff_message', models.TextField(null=True, blank=True)),
                 ('bans_version', models.PositiveIntegerField(default=0)),
                 ('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)),
                 ('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)),
                 ('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
 import re
 
 
 from django.conf import settings
 from django.conf import settings
 from django.db import models
 from django.db import models
 from django.utils import timezone
 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 import cachebuster
+from misago.core.utils import date_format
 
 
 
 
 __all__ = [
 __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):
 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):
     def invalidate_cache(self):
         cachebuster.invalidate(BAN_CACHEBUSTER)
         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:
         if username:
             username = username.lower()
             username = username.lower()
-            tests.append(BAN_USERNAME)
+            checks.append(BAN_USERNAME)
         if email:
         if email:
             email = email.lower()
             email = email.lower()
-            tests.append(BAN_EMAIL)
+            checks.append(BAN_EMAIL)
         if ip:
         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():
         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
                 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
                 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
                 return ban
         else:
         else:
-            raise Ban.DoesNotExist('no valid ban for values has been found')
+            raise Ban.DoesNotExist('specified values are not banned')
 
 
 
 
 class Ban(models.Model):
 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)
     banned_value = models.CharField(max_length=255, db_index=True)
     user_message = models.TextField(null=True, blank=True)
     user_message = models.TextField(null=True, blank=True)
     staff_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()
     objects = BansManager()
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
         self.banned_value = self.banned_value.lower()
         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)
         return super(Ban, self).save(*args, **kwargs)
 
 
     @property
     @property
-    def test_name(self):
-        return BANS_CHOICES[self.test][1]
+    def check_name(self):
+        return BANS_CHOICES[self.check_type][1]
 
 
     @property
     @property
     def name(self):
     def name(self):
@@ -98,12 +120,16 @@ class Ban(models.Model):
 
 
     @property
     @property
     def is_expired(self):
     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:
         else:
             return False
             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:
         if '*' in self.banned_value:
             regex = re.escape(self.banned_value).replace('\*', '(.*?)')
             regex = re.escape(self.banned_value).replace('\*', '(.*?)')
             return re.search('^%s$' % regex, value) is not None
             return re.search('^%s$' % regex, value) is not None
@@ -111,7 +137,7 @@ class Ban(models.Model):
             return self.banned_value == value
             return self.banned_value == value
 
 
     def lift(self):
     def lift(self):
-        self.valid_until = (timezone.now() - timedelta(days=1)).date()
+        self.expires_on = timezone.now()
 
 
 
 
 class BanCache(models.Model):
 class BanCache(models.Model):
@@ -122,7 +148,11 @@ class BanCache(models.Model):
     bans_version = models.PositiveIntegerField(default=0)
     bans_version = models.PositiveIntegerField(default=0)
     user_message = models.TextField(null=True, blank=True)
     user_message = models.TextField(null=True, blank=True)
     staff_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
     @property
     def is_banned(self):
     def is_banned(self):
@@ -132,7 +162,6 @@ class BanCache(models.Model):
     def is_valid(self):
     def is_valid(self):
         version_is_valid = cachebuster.is_valid(BAN_CACHEBUSTER,
         version_is_valid = cachebuster.is_valid(BAN_CACHEBUSTER,
                                                 self.bans_version)
                                                 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)
     user_ban = get_user_ban(user)
     if user_ban:
     if user_ban:
         user_state['is_banned'] = True
         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:
     try:
         if not user.is_hiding_presence or acl['can_see_hidden_users']:
         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 = get_user_model()
         User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
         User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
                                  requires_activation=1)
                                  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!')
                            user_message='Nope!')
 
 
         response = self.client.post(
         response = self.client.post(
@@ -61,7 +62,8 @@ class ActivationViewsTests(TestCase):
         User = get_user_model()
         User = get_user_model()
         test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
         test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
                                              requires_activation=1)
                                              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!')
                            user_message='Nope!')
 
 
         activation_token = make_activation_token(test_user)
         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"""
         """login view fails to sign banned user in"""
         User = get_user_model()
         User = get_user_model()
         User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
         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!')
                            user_message='Nope!')
 
 
         response = self.client.post(
         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):
 class BansManagerTests(TestCase):
     def setUp(self):
     def setUp(self):
         Ban.objects.bulk_create([
         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):
         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):
         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):
         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'}
         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'}
         invalid_kwargs = {'username': 'bsob', 'ip': '42.51.52.51'}
         with self.assertRaises(Ban.DoesNotExist):
         with self.assertRaises(Ban.DoesNotExist):
-            Ban.objects.find_ban(**invalid_kwargs)
+            Ban.objects.get_ban(**invalid_kwargs)
 
 
 
 
 class BanTests(TestCase):
 class BanTests(TestCase):
-    def test_test_value_literal(self):
+    def test_check_value_literal(self):
         """ban correctly tests given values"""
         """ban correctly tests given values"""
         test_ban = Ban(banned_value='bob')
         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"""
         """ban correctly tests given values"""
         test_ban = Ban(banned_value='bob*')
         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"""
         """ban correctly tests given values"""
         test_ban = Ban(banned_value='b*b')
         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"""
         """ban correctly tests given values"""
         test_ban = Ban(banned_value='*bob')
         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'])
         response = self.client.get(response['location'])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-    def test_mass_activation(self):
+    def test_mass_delete(self):
         """adminview deletes multiple bans"""
         """adminview deletes multiple bans"""
         for i in xrange(10):
         for i in xrange(10):
             response = self.client.post(
             response = self.client.post(
                 reverse('misago:admin:users:bans:new'),
                 reverse('misago:admin:users:bans:new'),
                 data={
                 data={
-                    'test': '1',
+                    'check_type': '1',
                     'banned_value': 'test@test.com',
                     'banned_value': 'test@test.com',
                     'user_message': 'Lorem ipsum dolor met',
                     'user_message': 'Lorem ipsum dolor met',
                     'staff_message': 'Sit amet elit',
                     '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)
         self.assertEqual(Ban.objects.count(), 10)
@@ -59,11 +59,11 @@ class BanAdminViewsTests(AdminTestCase):
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:users:bans:new'),
             reverse('misago:admin:users:bans:new'),
             data={
             data={
-                'test': '1',
+                'check_type': '1',
                 'banned_value': 'test@test.com',
                 'banned_value': 'test@test.com',
                 'user_message': 'Lorem ipsum dolor met',
                 'user_message': 'Lorem ipsum dolor met',
                 'staff_message': 'Sit amet elit',
                 '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)
         self.assertEqual(response.status_code, 302)
 
 
@@ -77,7 +77,7 @@ class BanAdminViewsTests(AdminTestCase):
         self.client.post(
         self.client.post(
             reverse('misago:admin:users:bans:new'),
             reverse('misago:admin:users:bans:new'),
             data={
             data={
-                'test': '0',
+                'check_type': '0',
                 'banned_value': 'Admin',
                 'banned_value': 'Admin',
             })
             })
 
 
@@ -86,11 +86,11 @@ class BanAdminViewsTests(AdminTestCase):
             reverse('misago:admin:users:bans:edit',
             reverse('misago:admin:users:bans:edit',
                     kwargs={'ban_id': test_ban.pk}),
                     kwargs={'ban_id': test_ban.pk}),
             data={
             data={
-                'test': '1',
+                'check_type': '1',
                 'banned_value': 'test@test.com',
                 'banned_value': 'test@test.com',
                 'user_message': 'Lorem ipsum dolor met',
                 'user_message': 'Lorem ipsum dolor met',
                 'staff_message': 'Sit amet elit',
                 '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)
         self.assertEqual(response.status_code, 302)
 
 
@@ -98,13 +98,14 @@ class BanAdminViewsTests(AdminTestCase):
         response = self.client.get(response['location'])
         response = self.client.get(response['location'])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertIn('test@test.com', response.content)
         self.assertIn('test@test.com', response.content)
+        #raise Exception('FIX WARNING!')
 
 
     def test_delete_view(self):
     def test_delete_view(self):
         """delete ban view has no showstoppers"""
         """delete ban view has no showstoppers"""
         self.client.post(
         self.client.post(
             reverse('misago:admin:users:bans:new'),
             reverse('misago:admin:users:bans:new'),
             data={
             data={
-                'test': '0',
+                'check_type': '0',
                 'banned_value': 'TestBan',
                 '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.contrib.auth import get_user_model
 from django.test import TestCase
 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.bans import get_user_ban, get_request_ip_ban
 from misago.users.models import Ban, BAN_IP
 from misago.users.models import Ban, BAN_IP
@@ -36,7 +37,7 @@ class UserBansTests(TestCase):
         Ban.objects.create(banned_value='bo*',
         Ban.objects.create(banned_value='bo*',
                            user_message='User reason',
                            user_message='User reason',
                            staff_message='Staff 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)
         user_ban = get_user_ban(self.user)
         self.assertIsNotNone(user_ban)
         self.assertIsNotNone(user_ban)
@@ -47,7 +48,7 @@ class UserBansTests(TestCase):
     def test_expired_ban(self):
     def test_expired_ban(self):
         """user is not caught by expired ban"""
         """user is not caught by expired ban"""
         Ban.objects.create(banned_value='bo*',
         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.assertIsNone(get_user_ban(self.user))
         self.assertFalse(self.user.ban_cache.is_banned)
         self.assertFalse(self.user.ban_cache.is_banned)
@@ -67,7 +68,7 @@ class RequestIPBansTests(TestCase):
 
 
     def test_permanent_ban(self):
     def test_permanent_ban(self):
         """ip is caught by permanent ban"""
         """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',
                            banned_value='127.0.0.1',
                            user_message='User reason')
                            user_message='User reason')
 
 
@@ -76,24 +77,33 @@ class RequestIPBansTests(TestCase):
         self.assertEqual(ip_ban['ip'], '127.0.0.1')
         self.assertEqual(ip_ban['ip'], '127.0.0.1')
         self.assertEqual(ip_ban['message'], 'User reason')
         self.assertEqual(ip_ban['message'], 'User reason')
 
 
+        # repeated call uses cache
+        get_request_ip_ban(FakeRequest())
+
     def test_temporary_ban(self):
     def test_temporary_ban(self):
         """ip is caught by temporary ban"""
         """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',
                            banned_value='127.0.0.1',
                            user_message='User reason',
                            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())
         ip_ban = get_request_ip_ban(FakeRequest())
         self.assertTrue(ip_ban['is_banned'])
         self.assertTrue(ip_ban['is_banned'])
         self.assertEqual(ip_ban['ip'], '127.0.0.1')
         self.assertEqual(ip_ban['ip'], '127.0.0.1')
         self.assertEqual(ip_ban['message'], 'User reason')
         self.assertEqual(ip_ban['message'], 'User reason')
 
 
+        # repeated call uses cache
+        get_request_ip_ban(FakeRequest())
+
     def test_expired_ban(self):
     def test_expired_ban(self):
         """ip is not caught by expired ban"""
         """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',
                            banned_value='127.0.0.1',
                            user_message='User reason',
                            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())
         ip_ban = get_request_ip_ban(FakeRequest())
         self.assertIsNone(ip_ban)
         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
         # create 5 bans then update their valid date to past one
         for i in xrange(5):
         for i in xrange(5):
             Ban.objects.create(banned_value="abcd")
             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()
         command = bansmaintenance.Command()
 
 
@@ -29,7 +29,7 @@ class BansMaintenanceTests(TestCase):
 
 
         self.assertEqual(command_output, 'Bans invalidated: 5')
         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):
     def test_bans_caches_updates(self):
         """ban caches are updated"""
         """ban caches are updated"""
@@ -42,7 +42,7 @@ class BansMaintenanceTests(TestCase):
         user_ban = bans.get_user_ban(user)
         user_ban = bans.get_user_ban(user)
 
 
         self.assertIsNotNone(user_ban)
         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
         # first call didn't touch ban
         command = bansmaintenance.Command()
         command = bansmaintenance.Command()
@@ -52,12 +52,12 @@ class BansMaintenanceTests(TestCase):
         command_output = out.getvalue().splitlines()[1].strip()
         command_output = out.getvalue().splitlines()[1].strip()
 
 
         self.assertEqual(command_output, 'Ban caches emptied: 0')
         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
         # 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
         # invalidate expired ban cache
         out = StringIO()
         out = StringIO()
@@ -65,7 +65,7 @@ class BansMaintenanceTests(TestCase):
         command_output = out.getvalue().splitlines()[1].strip()
         command_output = out.getvalue().splitlines()[1].strip()
 
 
         self.assertEqual(command_output, 'Ban caches emptied: 1')
         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
         # see if user is banned anymore
         user = User.objects.get(id=user.id)
         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"""
         """request new password view errors for banned users"""
         User = get_user_model()
         User = get_user_model()
         User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
         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!')
                            user_message='Nope!')
 
 
         response = self.client.post(
         response = self.client.post(
@@ -61,7 +62,8 @@ class ForgottenPasswordViewsTests(TestCase):
         test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
         test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
         old_password = test_user.password
         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!')
                            user_message='Nope!')
 
 
         password_token = make_password_reset_token(test_user)
         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):
 class ValidateEmailBannedTests(TestCase):
     def setUp(self):
     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):
     def test_unbanned_name(self):
         """unbanned email passes validation"""
         """unbanned email passes validation"""
@@ -97,7 +97,7 @@ class ValidateUsernameAvailableTests(TestCase):
 
 
 class ValidateUsernameBannedTests(TestCase):
 class ValidateUsernameBannedTests(TestCase):
     def setUp(self):
     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):
     def test_unbanned_name(self):
         """unbanned name passes validation"""
         """unbanned name passes validation"""
@@ -142,7 +142,3 @@ class ValidateUsernameLengthTests(TestCase):
             validate_username_length('a' * (settings.username_length_min - 1))
             validate_username_length('a' * (settings.username_length_min - 1))
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
             validate_username_length('a' * (settings.username_length_max + 1))
             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,
                         banned_value=user.username,
                         user_message=form.cleaned_data.get('user_message'),
                         user_message=form.cleaned_data.get('user_message'),
                         staff_message=form.cleaned_data.get('staff_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()
                 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())}
                 message_formats = {'date': date_format(timezone.now())}
                 staff_message = staff_message % message_formats
                 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
                 raise e
 
 
             activation_kwargs = {}
             activation_kwargs = {}