Browse Source

Further refractoring

Ralfp 12 years ago
parent
commit
7129d754e7
45 changed files with 1482 additions and 20 deletions
  1. 0 0
      misago/_migrations/__init__.py
  2. 0 0
      misago/admin/__init__.py
  3. 6 0
      misago/context_processors.py
  4. 0 0
      misago/core/__init__.py
  5. 4 0
      misago/core/forms/__init__.py
  6. 14 0
      misago/core/forms/fields.py
  7. 155 0
      misago/core/forms/forms.py
  8. 302 0
      misago/core/forms/layouts.py
  9. 7 0
      misago/core/forms/widgets.py
  10. 64 0
      misago/core/monitor.py
  11. 74 0
      misago/core/settings.py
  12. 7 0
      misago/core/stopwatch.py
  13. 0 0
      misago/fixtures/__init__.py
  14. 72 0
      misago/fixtures/basicsettings.py
  15. 14 0
      misago/fixtures/usersmonitor.py
  16. 0 0
      misago/front/__init__.py
  17. 0 0
      misago/management/__init__.py
  18. 0 0
      misago/management/commands/__init__.py
  19. 37 0
      misago/management/commands/about.py
  20. 15 0
      misago/management/commands/initmisago.py
  21. 48 0
      misago/management/commands/syncfixtures.py
  22. 14 0
      misago/management/commands/updmisago.py
  23. 0 0
      misago/middleware/__init__.py
  24. 5 0
      misago/middleware/monitor.py
  25. 5 0
      misago/middleware/settings.py
  26. 16 0
      misago/middleware/stopwatch.py
  27. 8 4
      misago/models/__init__.py
  28. 7 0
      misago/models/fixturemodel.py
  29. 9 0
      misago/models/monitoritemmodel.py
  30. 136 0
      misago/models/settingmodel.py
  31. 15 0
      misago/models/settingsgroupmodel.py
  32. 0 1
      misago/notrefactored/forms/__init__.py
  33. 11 0
      misago/notrefactored/utils/slugify.py
  34. 15 15
      misago/settings_base.py
  35. 0 0
      misago/shared/__init__.py
  36. 0 0
      misago/templatetags/__init__.py
  37. 0 0
      misago/utils/__init__.py
  38. 46 0
      misago/utils/avatars.py
  39. 108 0
      misago/utils/datesformats.py
  40. 115 0
      misago/utils/fixtures.py
  41. 30 0
      misago/utils/pagination.py
  42. 16 0
      misago/utils/strings.py
  43. 87 0
      misago/utils/timezones.py
  44. 20 0
      misago/utils/translation.py
  45. 0 0
      misago/validators/__init__.py

+ 0 - 0
misago/_migrations/__init__.py


+ 0 - 0
misago/admin/__init__.py


+ 6 - 0
misago/context_processors.py

@@ -0,0 +1,6 @@
+def misago(request):
+    return {
+        'monitor': request.monitor,
+        'settings': request.settings,
+        'stopwatch': request.stopwatch.time(),
+    }

+ 0 - 0
misago/core/__init__.py


+ 4 - 0
misago/core/forms/__init__.py

@@ -0,0 +1,4 @@
+from misago.core.forms.fields import ForumChoiceField
+from misago.core.forms.forms import Form
+from misago.core.forms.layouts import FormLayout, FormFields, FormFieldsets
+from misago.core.forms.widgets import YesNoSwitch

+ 14 - 0
misago/core/forms/fields.py

@@ -0,0 +1,14 @@
+from django.utils.html import conditional_escape, mark_safe
+from mptt.forms import TreeNodeChoiceField
+
+class ForumChoiceField(TreeNodeChoiceField):
+    """
+    Custom forum choice field
+    """
+    def __init__(self, *args, **kwargs):
+        kwargs['level_indicator'] = u'- - '
+        super(ForumChoiceField, self).__init__(*args, **kwargs)
+
+    def _get_level_indicator(self, obj):
+        level = getattr(obj, obj._mptt_meta.level_attr)
+        return mark_safe(conditional_escape(self.level_indicator) * (level - 1))

+ 155 - 0
misago/core/forms/forms.py

@@ -0,0 +1,155 @@
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+from recaptcha.client.captcha import submit as recaptcha_submit
+
+class Form(forms.Form):
+    """
+    Misago-native form abstract extending Django's one with automatic trimming
+    of user input, captacha support and more accessible validation errors
+    """
+    validate_repeats = []
+    repeats_errors = []
+    dont_strip = []
+    error_source = None
+
+    def __init__(self, data=None, file=None, request=None, *args, **kwargs):
+        self.request = request
+
+        # Extract request from first argument
+        if data != None:
+            super(Form, self).__init__(data, file, *args, **kwargs)
+        else:
+            super(Form, self).__init__(*args, **kwargs)
+
+        # Kill captcha fields
+        try:
+            if self.request.settings['bots_registration'] != 'recaptcha' or self.request.session.get('captcha_passed'):
+                del self.fields['recaptcha']
+        except KeyError:
+            pass
+        try:
+            if self.request.settings['bots_registration'] != 'qa' or self.request.session.get('captcha_passed'):
+                del self.fields['captcha_qa']
+            else:
+                # Make sure we have any questions loaded
+                self.fields['captcha_qa'].label = self.request.settings['qa_test']
+                self.fields['captcha_qa'].help_text = self.request.settings['qa_test_help']
+        except KeyError:
+            pass
+
+        # Let forms do mumbo-jumbo with fields removing
+        self.finalize_form()
+
+    def finalize_form(self):
+        pass
+
+    def full_clean(self):
+        """
+        Trim inputs and strip newlines
+        """
+        self.data = self.data.copy()
+        for key, field in self.fields.iteritems():
+            try:
+                if field.__class__.__name__ in ['ModelChoiceField', 'TreeForeignKey'] and self.data[key]:
+                    self.data[key] = int(self.data[key])
+                elif field.__class__.__name__ == 'ModelMultipleChoiceField':
+                    self.data.setlist(key, [int(x) for x in self.data.getlist(key, [])])
+                elif field.__class__.__name__ not in ['DateField', 'DateTimeField']:
+                    if not key in self.dont_strip:
+                        if field.__class__.__name__ in ['MultipleChoiceField', 'TypedMultipleChoiceField']:
+                            self.data.setlist(key, [x.strip() for x in self.data.getlist(key, [])])
+                        else:
+                            self.data[key] = self.data[key].strip()
+                    if field.__class__.__name__ in ['MultipleChoiceField', 'TypedMultipleChoiceField']:
+                        self.data.setlist(key, [x.replace("\r\n", '') for x in self.data.getlist(key, [])])
+                    elif not field.widget.__class__.__name__ in ['Textarea']:
+                        self.data[key] = self.data[key].replace("\r\n", '')
+            except (KeyError, AttributeError):
+                pass
+        super(Form, self).full_clean()
+
+    def clean(self):
+        """
+        Clean data, do magic checks and stuff
+        """
+        cleaned_data = super(Form, self).clean()
+        self._check_all()
+        return cleaned_data
+
+    def clean_recaptcha(self):
+        """
+        Test reCaptcha, scream if it went wrong
+        """
+        response = recaptcha_submit(
+                                    self.request.POST.get('recaptcha_challenge_field'),
+                                    self.request.POST.get('recaptcha_response_field'),
+                                    self.request.settings['recaptcha_private'],
+                                    self.request.session.get_ip(self.request)
+                                    ).is_valid
+        if not response:
+            raise forms.ValidationError(_("Entered words are incorrect. Please try again."))
+        self.request.session['captcha_passed'] = True
+        return ''
+
+    def clean_captcha_qa(self):
+        """
+        Test QA Captcha, scream if it went wrong
+        """
+
+        if not unicode(self.cleaned_data['captcha_qa']).lower() in (name.lower() for name in unicode(self.request.settings['qa_test_answers']).splitlines()):
+            raise forms.ValidationError(_("The answer you entered is incorrect."))
+        self.request.session['captcha_passed'] = True
+        return self.cleaned_data['captcha_qa']
+
+    def _check_all(self):
+        # Check repeated fields
+        self._check_repeats()
+        # Check CSRF, we dont allow un-csrf'd forms in Misago
+        self._check_csrf()
+        # Check if we have any errors from fields, if we do, we will set fancy form-wide error message
+        self._check_fields_errors()
+
+    def _check_repeats(self):
+        for index, repeat in enumerate(self.validate_repeats):
+            # Check empty fields
+            for field in repeat:
+                if not field in self.data:
+                    try:
+                        if len(repeat) == 2:
+                            self.errors['_'.join(repeat)] = [self.repeats_errors[index]['fill_both']]
+                        else:
+                            self.errors['_'.join(repeat)] = [self.repeats_errors[index]['fill_all']]
+                    except (IndexError, KeyError):
+                        if len(repeat) == 2:
+                            self.errors['_'.join(repeat)] = [_("You have to fill in both fields.")]
+                        else:
+                            self.errors['_'.join(repeat)] = [_("You have to fill in all fields.")]
+                    break
+
+            else:
+                # Check different fields
+                past_field = self.data[repeat[0]]
+                for field in repeat:
+                    if self.data[field] != past_field:
+                        try:
+                            self.errors['_'.join(repeat)] = [self.repeats_errors[index]['different']]
+                        except (IndexError, KeyError):
+                            self.errors['_'.join(repeat)] = [_("Entered values differ from each other.")]
+                        break
+                    past_field = self.data[field]
+
+
+    def _check_csrf(self):
+        if not self.request.csrf.request_secure(self.request):
+            raise forms.ValidationError(_("Request authorization is invalid. Please resubmit your form."))
+
+    def _check_fields_errors(self):
+        if self.errors:
+            if self.error_source and self.error_source in self.errors:
+                field_error, self.errors[self.error_source] = self.errors[self.error_source][0], []
+                raise forms.ValidationError(field_error)
+            raise forms.ValidationError(_("Form contains errors."))
+        
+    def empty_errors(self):
+        for i in self.errors:
+            self.errors[i] = []

+ 302 - 0
misago/core/forms/layouts.py

@@ -0,0 +1,302 @@
+from UserDict import IterableUserDict
+from django.utils import formats
+
+class FormLayout(object):
+    """
+    Conglomelate of fields and fieldsets describing form structure
+    """
+    def __init__(self, form, fieldsets=False):
+        scaffold_fields = FormFields(form)
+        scaffold_fieldsets = FormFieldsets(form, scaffold_fields.fields, fieldsets)
+
+        self.multipart_form = scaffold_fields.multipart_form
+        self.fieldsets = scaffold_fieldsets.fieldsets
+        self.fields = scaffold_fields.fields
+        self.hidden = scaffold_fields.hidden
+
+
+class FormFields(object):
+    """
+    Hydrator that builds fields list from form and blueprint
+    """
+    def __init__(self, form):
+        self.multipart_form = False
+        self.fields = {}
+        self.hidden = []
+
+        # Extract widgets from meta
+        self.meta_widgets = {}
+        try:
+            self.meta_widgets = form.Meta.widgets
+        except AttributeError:
+            pass
+
+        # Find out field input types
+        for field in form.fields.keys():
+            widget = self._get_widget(field, form.fields[field])
+            widget_name = widget.__class__.__name__
+            bound_field = form[field]
+            blueprint = {
+                         'attrs': {
+                                    'id': bound_field.auto_id,
+                                    'name': bound_field.html_name,
+                                   },
+                         'endrow': False,
+                         'errors': [],
+                         'has_value': bound_field.value() != None,
+                         'help_text': bound_field.help_text,
+                         'hidden': widget.is_hidden,
+                         'html_id': bound_field.auto_id,
+                         'html_name': bound_field.html_name,
+                         'id': field,
+                         'initial': bound_field.field.initial,
+                         'label': bound_field.label,
+                         'last': False,
+                         'nested': [],
+                         'required': bound_field.field.widget.is_required,
+                         'show_hidden_initial': bound_field.field.show_hidden_initial,
+                         'value': bound_field.value(),
+                         'width': 100,
+                         'widget': '',
+                         'choices': [],
+                        }
+
+            # Set multipart form
+            if widget.needs_multipart_form:
+                self.multipart_form = True
+
+            # Get errors?
+            if form.is_bound:
+                for error in bound_field._errors():
+                    blueprint['errors'].append(error)
+                try:
+                    for error in form.errors[field]:
+                        if not error in blueprint['errors']:
+                            blueprint['errors'].append(error)
+                except KeyError:
+                    pass
+
+            # Use clean value instead?
+            try:
+                if field in form.cleaned_data:
+                    blueprint['value'] = form.cleaned_data[field]
+            except AttributeError:
+                pass
+
+            # TextInput
+            if widget_name in ['TextInput', 'PasswordInput', 'Textarea']:
+                blueprint['widget'] = 'text'
+                blueprint['attrs']['type'] = 'text'
+                try:
+                    blueprint['attrs']['maxlength'] = bound_field.field.max_length
+                except AttributeError:
+                    pass
+
+            # PasswordInput
+            if widget_name == 'PasswordInput':
+                blueprint['attrs']['type'] = 'password'
+
+            # Textarea      
+            if widget_name == 'Textarea':
+                blueprint['widget'] = 'textarea'
+
+            # ReCaptcha      
+            if widget_name == 'ReCaptchaWidget':
+                from recaptcha.client.captcha import displayhtml
+                blueprint['widget'] = 'recaptcha'
+                blueprint['attrs'] = {'html': displayhtml(
+                                                          form.request.settings['recaptcha_public'],
+                                                          form.request.settings['recaptcha_ssl'],
+                                                          bound_field.field.api_error,
+                                                          )}
+
+            # HiddenInput
+            if widget_name == 'HiddenInput':
+                blueprint['widget'] = 'hidden'
+
+            # MultipleHiddenInput
+            if widget_name == 'MultipleHiddenInput':
+                blueprint['widget'] = 'multiple_hidden'
+                blueprint['attrs'] = {
+                                      'choices': widget.choices
+                                     }
+
+            # FileInput
+            if widget_name == 'FileInput':
+                blueprint['widget'] = 'file'
+
+            # ClearableFileInput
+            if widget_name == 'ClearableFileInput':
+                blueprint['widget'] = 'file_clearable'
+
+            # DateInput
+            if widget_name == 'DateInput':
+                blueprint['widget'] = 'date'
+                try:
+                    blueprint['value'] = blueprint['value'].strftime('%Y-%m-%d')
+                except AttributeError as e:
+                    pass
+
+            # DateTimeInput
+            if widget_name == 'DateTimeInput':
+                blueprint['widget'] = 'datetime'
+                try:
+                    blueprint['value'] = blueprint['value'].strftime('%Y-%m-%d %H:%M')
+                except AttributeError as e:
+                    pass
+
+            # TimeInput
+            if widget_name == 'TimeInput':
+                blueprint['widget'] = 'time'
+                try:
+                    blueprint['value'] = blueprint['value'].strftime('%H:%M')
+                except AttributeError as e:
+                    pass
+
+            # CheckboxInput
+            if widget_name == 'CheckboxInput':
+                blueprint['widget'] = 'checkbox'
+
+            # Select, NullBooleanSelect, SelectMultiple, RadioSelect, CheckboxSelectMultiple
+            if widget_name in ['Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple']:
+                blueprint['choices'] = widget.choices
+
+            # Yes-no radio select
+            if widget_name == 'YesNoSwitch':
+                blueprint['widget'] = 'yes_no_switch'
+
+            # Select
+            if widget_name == 'Select':
+                blueprint['widget'] = 'select'
+                if not blueprint['value']:
+                    blueprint['value'] = None
+
+            # NullBooleanSelect
+            if widget_name == 'NullBooleanSelect':
+                blueprint['widget'] = 'null_boolean_select'
+
+            # SelectMultiple
+            if widget_name == 'SelectMultiple':
+                blueprint['widget'] = 'select_multiple'
+
+            # RadioSelect
+            if widget_name == 'RadioSelect':
+                blueprint['widget'] = 'radio_select'
+                if not blueprint['value']:
+                    blueprint['value'] = u''
+
+            # CheckboxSelectMultiple
+            if widget_name == 'CheckboxSelectMultiple':
+                blueprint['widget'] = 'checkbox_select_multiple'
+
+            # MultiWidget
+            if widget_name == 'MultiWidget':
+                blueprint['widget'] = 'multi'
+
+            # SplitDateTimeWidget
+            if widget_name == 'SplitDateTimeWidget':
+                blueprint['widget'] = 'split_datetime'
+
+            # SplitHiddenDateTimeWidget
+            if widget_name == 'SplitHiddenDateTimeWidget':
+                blueprint['widget'] = 'split_hidden_datetime'
+
+            # SelectDateWidget
+            if widget_name == 'SelectDateWidget':
+                blueprint['widget'] = 'select_date'
+                blueprint['years'] = widget.years
+
+            # Store field in either of collections
+            if blueprint['hidden']:
+                blueprint['attrs']['type'] = 'hidden'
+                self.hidden.append(blueprint)
+            else:
+                self.fields[field] = blueprint
+
+    def _get_widget(self, name, field):
+        if name in self.meta_widgets:
+            return self.meta_widgets[name]
+        return field.widget
+
+
+class FormFieldsets(object):
+    """
+    Hydrator that builds fieldset from form and blueprint
+    """
+    def __init__(self, form, fields, fieldsets=None):
+        self.fieldsets = []
+
+        # Use form layout
+        if not fieldsets:
+            try:
+                fieldsets = form.layout
+            except AttributeError:
+                pass
+
+        # Build fieldsets data
+        if fieldsets:
+            for blueprint in fieldsets:
+                fieldset = {'legend': None, 'fields': [], 'help': None, 'last': False}
+                fieldset['legend'] = blueprint[0]
+                row_width = 0
+                for field in blueprint[1]:
+                    try:
+                        if isinstance(field, basestring):
+                            fieldset['fields'].append(fields[field])
+                        elif field[0] == 'nested':
+                            subfields = {'label': None, 'help_text': None, 'nested': [], 'errors':[], 'endrow': False, 'last': False, 'width': 100}
+                            subfiels_ids = []
+                            try:
+                                subfields = field[2].update(subfields)
+                            except IndexError:
+                                pass
+                            for subfield in field[1]:
+                                if isinstance(subfield, basestring):
+                                    subfiels_ids.append(subfield)
+                                    subfields['nested'].append(fields[subfield])
+                                    for error in fields[subfield]['errors']:
+                                        if not error in subfields['errors']:
+                                            subfields['errors'].append(error)
+                                else:
+                                    subfiels_ids.append(subfield[0])
+                                    try:
+                                        subfield[1]['attrs'] = dict(fields[subfield[0]]['attrs'], **subfield[1]['attrs'])
+                                    except KeyError:
+                                        pass
+                                    subfields['nested'].append(dict(fields[subfield[0]], **subfield[1]))
+                                    for error in fields[subfield[0]]['errors']:
+                                        if not error in subfields['errors']:
+                                            subfields['errors'].append(error)
+                            if not subfields['label']:
+                                subfields['label'] = subfields['nested'][0]['label']
+                            if not subfields['help_text']:
+                                subfields['help_text'] = subfields['nested'][0]['help_text']
+                            try:
+                                subfields['errors'] = form.errors["_".join(subfiels_ids)]
+                            except KeyError:
+                                pass
+                            fieldset['fields'].append(subfields)
+                        else:
+                            try:
+                                field[1]['attrs'] = dict(fields[field[0]]['attrs'], **field[1]['attrs'])
+                            except KeyError:
+                                pass
+                            fieldset['fields'].append(dict(fields[field[0]], **field[1]))
+                        row_width += fieldset['fields'][-1]['width']
+                        if row_width >= 100:
+                            fieldset['fields'][-1]['endrow'] = True
+                            row_width = 0
+                    except (AttributeError, IndexError, KeyError):
+                        pass
+                if fieldset['fields']:
+                    fieldset['fields'][-1]['endrow'] = True
+                    fieldset['fields'][-1]['last'] = True
+                try:
+                    fieldset['help'] = blueprint[2]
+                except IndexError:
+                    pass
+
+                # Append complete fieldset
+                if fieldset['fields']:
+                    self.fieldsets.append(fieldset)
+            self.fieldsets[-1]['last'] = True

+ 7 - 0
misago/core/forms/widgets.py

@@ -0,0 +1,7 @@
+from django import forms
+
+class YesNoSwitch(forms.CheckboxInput):
+    """
+    Custom Yes-No switch as fancier alternative to checkboxes
+    """
+    pass

+ 64 - 0
misago/core/monitor.py

@@ -0,0 +1,64 @@
+from django.core.cache import cache
+from django.utils import timezone
+from misago.models import MonitorItem
+
+class Monitor(object):
+    def __init__(self):
+        self._cache_deleted = False
+        self._items = {}
+        self.refresh()
+
+    def refresh(self):
+        self._items = cache.get('monitor')
+        if not self._items:
+            self._items = {}
+            for i in MonitorItem.objects.all():
+                self._items[i.id] = [i.value, i.updated]
+            cache.set('monitor', self._items)
+
+    def __contains__(self, key):
+        return key in self._items
+
+    def __getitem__(self, key):
+        return self._items[key][0]
+
+    def __setitem__(self, key, value):
+        self._items[key][0] = value
+        cache.set('monitor', self._items)
+        sync_item = MonitorItem(id=key, value=value, updated=timezone.now())
+        sync_item.save(force_update=True)
+        return value
+
+    def __delitem__(self, key):
+        pass
+
+    def get(self, key, default=None):
+        if not key in self._items:
+            return default
+        return self._items[key][0]
+
+    def get_updated(self, key):
+        if key in self._items:
+            return self._items[key][1]
+        return None
+
+    def has_key(self, key):
+        return key in self._items
+
+    def keys(self):
+        return self._items.keys()
+
+    def values(self):
+        return self._items.values()
+
+    def items(self):
+        return self._items.items()
+
+    def iterkeys(self):
+        return self._items.iterkeys()
+
+    def itervalues(self):
+        return self._items.itervalues()
+
+    def iteritems(self):
+        return self._items.iteritems()

+ 74 - 0
misago/core/settings.py

@@ -0,0 +1,74 @@
+from django.db.utils import DatabaseError
+from django.core.cache import cache
+from misago.models import Setting
+
+class DBSettings(object):
+    """
+    Database-stored high-level and "safe" settings controller
+    """
+    def __init__(self):
+        self._settings = {}
+        self._models = {}
+        self.refresh()
+
+    def refresh(self):
+        self._models = cache.get('settings')
+        if not self._models:
+            self._models = {}
+            try:
+                for i in Setting.objects.all():
+                    self._models[i.pk] = i
+                    self._settings[i.pk] = i.get_value()
+                cache.set('settings', self._models)
+            except DatabaseError:
+                pass
+        else:
+            for i, model in self._models.items():
+                self._settings[i] = model.get_value()
+
+    def __getattr__(self, key):
+        return self._settings[key]
+
+    def __contains__(self, key):
+        return key in self._settings.keys()
+
+    def __getitem__(self, key):
+        return self._settings[key]
+
+    def __setitem__(self, key, value):
+        if key in self._settings.keys():
+            self._models[key].set_value(value)
+            self._models[key].save(force_update=True)
+            self._settings[key] = value
+            cache.set('settings', self._models)
+        return value
+
+    def __delitem__(self, key):
+        pass
+
+    def get(self, key, default=None):
+        try:
+            return self._settings[key]
+        except KeyError:
+            return None
+
+    def has_key(self, key):
+        return key in self._settings.keys()
+
+    def keys(self):
+        return self._settings.keys()
+
+    def values(self):
+        return self._settings.values()
+
+    def items(self):
+        return self._settings.items()
+
+    def iterkeys(self):
+        return self._settings.iterkeys()
+
+    def itervalues(self):
+        return self._settings.itervalues()
+
+    def iteritems(self):
+        return self._settings.iteritems()

+ 7 - 0
misago/core/stopwatch.py

@@ -0,0 +1,7 @@
+import time
+
+class Stopwatch(object):
+    def __init__(self):
+        self.start_time = time.time()
+    def time(self):
+        return time.time() - self.start_time 

+ 0 - 0
misago/fixtures/__init__.py


+ 72 - 0
misago/fixtures/basicsettings.py

@@ -0,0 +1,72 @@
+from misago.utils.fixtures import load_settings_fixture, update_settings_fixture
+from misago.utils.translation import ugettext_lazy as _
+
+settings_fixture = (
+   # Basic options
+   ('basic', {
+        'name': _("Basic Settings"),
+        'settings': (
+            ('board_name', {
+                'value':        "Misago",
+                'type':         "string",
+                'input':        "text",
+                'separator':    _("Board Name"),
+                'name':         _("Board Name"),
+            }),
+            ('board_header', {
+                'type':         "string",
+                'input':        "text",
+                'name':         _("Board Header"),
+                'description':  _("Some themes allow you to define text in board header. Leave empty to use Board Name instead."),
+            }),
+            ('board_header_postscript', {
+                'value':        "Work in progress",
+                'type':         "string",
+                'input':        "text",
+                'name':         _("Board Header Postscript"),
+                'description':  _("Additional text displayed in some themes board header after board name."),
+            }),
+            ('board_index_title', {
+                'type':         "string",
+                'input':        "text",
+                'separator':    _("Board Index"),
+                'name':         _("Board Index Title"),
+                'description':  _("If you want to, you can replace page title content on Board Index with custom one."),
+            }),
+            ('board_index_meta', {
+                'type':         "string",
+                'input':        "text",
+                'name':         _("Board Index Meta-Description"),
+                'description':  _("Meta-Description used to describe your board's index page."),
+            }),
+            ('board_credits', {
+                'type':         "string",
+                'input':        "textarea",
+                'separator':    _("Board Footer"),
+                'name':         _("Custom Credit"),
+                'description':  _("Custom Credit to display in board footer above software and theme copyright information. You can use HTML."),
+            }),
+            ('email_footnote', {
+                'type':         "string",
+                'input':        "textarea",
+                'separator':    _("Board E-Mails"),
+                'name':         _("Custom Footnote in HTML E-mails"),
+                'description':  _("Custom Footnote to display in HTML e-mail messages sent by board."),
+            }),
+            ('email_footnote_plain', {
+                'type':         "string",
+                'input':        "textarea",
+                'name':         _("Custom Footnote in plain text E-mails"),
+                'description':  _("Custom Footnote to display in plain text e-mail messages sent by board."),
+            }),
+        ),
+   }),
+)
+
+
+def load():
+    load_settings_fixture(settings_fixture)
+
+
+def update():
+    update_settings_fixture(settings_fixture)

+ 14 - 0
misago/fixtures/usersmonitor.py

@@ -0,0 +1,14 @@
+from misago.utils.fixtures import load_monitor_fixture
+
+monitor_fixture = {
+                   'users': 0,
+                   'users_inactive': 0,
+                   'users_reported': 0,
+                   'last_user': None,
+                   'last_user_name': None,
+                   'last_user_slug': None,
+                  }
+
+
+def load():
+    load_monitor_fixture(monitor_fixture)

+ 0 - 0
misago/front/__init__.py


+ 0 - 0
misago/management/__init__.py


+ 0 - 0
misago/management/commands/__init__.py


+ 37 - 0
misago/management/commands/about.py

@@ -0,0 +1,37 @@
+from django.core.management.base import BaseCommand, CommandError
+from django.utils import timezone
+from misago import get_version
+
+class Command(BaseCommand):
+    """
+    Displays version number and license
+    """
+    help = 'Displays Misago Credits'
+    def handle(self, *args, **options):
+        self.stdout.write('\n')
+        self.stdout.write('                                    _\n')
+        self.stdout.write('                         ____ ___  (_)________  ____  ____ \n')
+        self.stdout.write('                        / __ `__ \/ / ___/ __ `/ __ `/ __ \ \n')
+        self.stdout.write('                       / / / / / / (__  ) /_/ / /_/ / /_/ / \n')
+        self.stdout.write('                      /_/ /_/ /_/_/____/\__,_/\__, /\____/ \n')
+        self.stdout.write('                                             /____/\n')
+        self.stdout.write('\n')
+        self.stdout.write('                    Your community is powered by Misago v.%s' % get_version())
+        self.stdout.write('\n              For help and feedback visit http://misago-project.org')
+        self.stdout.write('\n\n')
+        self.stdout.write('================================================================================')
+        self.stdout.write('\n\n')
+        self.stdout.write('Copyright (C) %s, Rafal Piton' % timezone.now().year)
+        self.stdout.write('\n')
+        self.stdout.write('\nThis program is free software; you can redistribute it and/or modify it under')
+        self.stdout.write('\nthe terms of the GNU General Public License version 3 as published by')
+        self.stdout.write('\nthe Free Software Foundation')
+        self.stdout.write('\n')
+        self.stdout.write('\nThis program is distributed in the hope that it will be useful, but')
+        self.stdout.write('\nWITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY')
+        self.stdout.write('\nor FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License')
+        self.stdout.write('\nfor more details.')
+        self.stdout.write('\n')
+        self.stdout.write('\nYou should have received a copy of the GNU General Public License along')
+        self.stdout.write('\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.')
+        self.stdout.write('\n\n')

+ 15 - 0
misago/management/commands/initmisago.py

@@ -0,0 +1,15 @@
+from django.core.management import call_command
+from django.core.management.base import BaseCommand, CommandError
+
+class Command(BaseCommand):
+    """
+    Builds Misago database from scratch
+    """
+    help = 'Install Misago to database'
+    
+    def handle(self, *args, **options):
+        self.stdout.write('\nInstalling Misago to database...')
+        call_command('syncdb')
+        call_command('migrate')
+        call_command('syncfixtures')
+        self.stdout.write('\nInstallation complete! Don\'t forget to run adduser to create first admin!\n')

+ 48 - 0
misago/management/commands/syncfixtures.py

@@ -0,0 +1,48 @@
+from optparse import make_option
+import os.path
+import pkgutil
+from django.core.management.base import BaseCommand
+from misago.models import Fixture
+from misago.utils.fixtures import load_fixture, update_fixture
+import misago.fixtures
+
+class Command(BaseCommand):
+    """
+    Loads Misago fixtures
+    """
+    help = 'Load Misago fixtures'
+    option_list = BaseCommand.option_list + (
+        make_option('--quiet',
+            action='store_true',
+            dest='quiet',
+            default=False,
+            help='Dont display output from this message'),
+        )
+    
+    def handle(self, *args, **options):
+        if not options['quiet']:
+            self.stdout.write('\nLoading data from fixtures...')
+            
+        fixture_data = {}
+        for fixture in Fixture.objects.all():
+            fixture_data[fixture.name] = fixture
+
+        loaded = 0
+        updated = 0
+        
+        fixtures_path = os.path.dirname(misago.fixtures.__file__)
+        for _, name, _ in pkgutil.iter_modules([fixtures_path]):
+            if name in fixture_data:
+                if update_fixture('misago.fixtures.' + name):
+                    updated += 1
+                    if not options['quiet']:
+                        self.stdout.write('Updating "%s" fixture...' % name)
+            else:
+                if load_fixture('misago.fixtures.' + name):
+                    loaded += 1
+                    Fixture.objects.create(name=name)
+                    if not options['quiet']:
+                        self.stdout.write('Loading "%s" fixture...' % name)
+
+        if not options['quiet']:
+            self.stdout.write('\nLoaded %s fixtures and updated %s fixtures.\n' % (loaded, updated))

+ 14 - 0
misago/management/commands/updmisago.py

@@ -0,0 +1,14 @@
+from django.core.management import call_command
+from django.core.management.base import BaseCommand, CommandError
+
+class Command(BaseCommand):
+    """
+    Updates Misago to latest version
+    """
+    help = 'Update Misago database to latest version'
+    
+    def handle(self, *args, **options):
+        self.stdout.write('\nUpdating Misago database to latest version...')
+        call_command('migrate')
+        call_command('syncfixtures')
+        self.stdout.write('\nUpdate complete!\n')

+ 0 - 0
misago/middleware/__init__.py


+ 5 - 0
misago/middleware/monitor.py

@@ -0,0 +1,5 @@
+from misago.core.monitor import Monitor
+
+class MonitorMiddleware(object):
+    def process_request(self, request):
+        request.monitor = Monitor()

+ 5 - 0
misago/middleware/settings.py

@@ -0,0 +1,5 @@
+from misago.core.settings import DBSettings
+
+class SettingsMiddleware(object):
+    def process_request(self, request):
+        request.settings = DBSettings()

+ 16 - 0
misago/middleware/stopwatch.py

@@ -0,0 +1,16 @@
+from django.conf import settings
+from misago.core.stopwatch import Stopwatch
+
+class StopwatchMiddleware(object):
+    def process_request(self, request):
+        request.stopwatch = Stopwatch()
+
+    def process_response(self, request, response):
+        try:
+            if settings.STOPWATCH_LOG:
+                stat_file = open(settings.STOPWATCH_LOG, 'a')
+                stat_file.write("%s %s s\n" % (request.path_info, request.stopwatch.time()))
+                stat_file.close()
+        except AttributeError:
+            pass
+        return response

+ 8 - 4
misago/models/__init__.py

@@ -1,4 +1,8 @@
-from misago.models.alertmodel import Alert
-from misago.models.banmodel import Ban
-from misago.models.forumrolemodel import ForumRole
-from misago.models.signinattemptmodel import SignInAttempt
+#from misago.models.alertmodel import Alert
+#from misago.models.banmodel import Ban
+from misago.models.fixturemodel import Fixture
+#from misago.models.forumrolemodel import ForumRole
+from misago.models.monitoritemmodel import MonitorItem
+from misago.models.settingmodel import Setting
+from misago.models.settingsgroupmodel import SettingsGroup
+#from misago.models.signinattemptmodel import SignInAttempt

+ 7 - 0
misago/models/fixturemodel.py

@@ -0,0 +1,7 @@
+from django.db import models
+
+class Fixture(models.Model):
+    name = models.CharField(max_length=255)
+
+    class Meta:
+        app_label = 'misago'

+ 9 - 0
misago/models/monitoritemmodel.py

@@ -0,0 +1,9 @@
+from django.db import models
+
+class MonitorItem(models.Model):
+    id = models.CharField(max_length=255, primary_key=True)
+    value = models.TextField(blank=True, null=True)
+    updated = models.DateTimeField(blank=True, null=True)
+
+    class Meta:
+        app_label = 'misago'

+ 136 - 0
misago/models/settingmodel.py

@@ -0,0 +1,136 @@
+import base64
+from django import forms
+from django.core import validators
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from misago.core.forms import YesNoSwitch
+from misago.utils.timezones import tzlist
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+class Setting(models.Model):
+    setting = models.CharField(max_length=255, primary_key=True)
+    group = models.ForeignKey('SettingsGroup', to_field='key')
+    value = models.TextField(null=True, blank=True)
+    value_default = models.TextField(null=True, blank=True)
+    normalize_to = models.CharField(max_length=255)
+    field = models.CharField(max_length=255)
+    extra = models.TextField(null=True, blank=True)
+    position = models.IntegerField(default=0)
+    separator = models.CharField(max_length=255, null=True, blank=True)
+    name = models.CharField(max_length=255)
+    description = models.TextField(null=True, blank=True)
+
+    class Meta:
+        app_label = 'misago'
+
+    def get_extra(self):
+        return pickle.loads(base64.decodestring(self.extra))
+
+    def get_value(self):
+        if self.normalize_to == 'array':
+            return self.value.split(',')
+        if self.normalize_to == 'integer':
+            return int(self.value)
+        if self.normalize_to == 'float':
+            return float(self.value)
+        if self.normalize_to == 'boolean':
+            return self.value == "1"
+        return self.value
+
+    def set_value(self, value):
+        if self.normalize_to == 'array':
+            self.value = ','.join(value)
+        elif self.normalize_to == 'integer':
+            self.value = int(value)
+        elif self.normalize_to == 'float':
+            self.value = float(value)
+        elif self.normalize_to == 'boolean':
+            self.value = 1 if value else 0
+        else:
+            self.value = value
+        if not self.value and self.value_default:
+            self.value = self.value_default
+        return self.value
+
+    def get_field(self):
+        extra = self.get_extra()
+
+        # Set validators
+        field_validators = []
+        if 'min' in extra:
+            if self.normalize_to == 'string' or self.normalize_to == 'array':
+                field_validators.append(validators.MinLengthValidator(extra['min']))
+            if self.normalize_to == 'integer' or self.normalize_to == 'float':
+                field_validators.append(validators.MinValueValidator(extra['min']))
+        if 'max' in extra:
+            if self.normalize_to == 'string' or self.normalize_to == 'array':
+                field_validators.append(validators.MaxLengthValidator(extra['max']))
+            if self.normalize_to == 'integer' or self.normalize_to == 'float':
+                field_validators.append(validators.MaxValueValidator(extra['max']))
+
+        # Yes-no
+        if self.field == 'yesno':
+            return forms.BooleanField(
+                                   initial=self.get_value(),
+                                   label=_(self.name),
+                                   help_text=_(self.description) if self.description else None,
+                                   required=False,
+                                   widget=YesNoSwitch,
+                                   )
+
+        # Multi-list
+        if self.field == 'mlist':
+            return forms.MultipleChoiceField(
+                                     initial=self.get_value(),
+                                     label=_(self.name),
+                                     help_text=_(self.description) if self.description else None,
+                                     widget=forms.CheckboxSelectMultiple,
+                                     validators=field_validators,
+                                     required=False,
+                                     choices=extra['choices']
+                                     )
+
+        # Select or choice
+        if self.field == 'select' or self.field == 'choice':
+            # Timezone list?
+            if extra['choices'] == '#TZ#':
+                extra['choices'] = tzlist()
+            return forms.ChoiceField(
+                                     initial=self.get_value(),
+                                     label=_(self.name),
+                                     help_text=_(self.description) if self.description else None,
+                                     widget=forms.RadioSelect if self.field == 'choice' else forms.Select,
+                                     validators=field_validators,
+                                     required=False,
+                                     choices=extra['choices']
+                                     )
+
+        # Textarea
+        if self.field == 'textarea':
+            return forms.CharField(
+                                   initial=self.get_value(),
+                                   label=_(self.name),
+                                   help_text=_(self.description) if self.description else None,
+                                   validators=field_validators,
+                                   required=False,
+                                   widget=forms.Textarea
+                                   )
+
+        # Default input
+        default_input = forms.CharField
+        if self.normalize_to == 'integer':
+            default_input = forms.IntegerField
+        if self.normalize_to == 'float':
+            default_input = forms.FloatField
+
+        # Make text-input
+        return default_input(
+                             initial=self.get_value(),
+                             label=_(self.name),
+                             help_text=_(self.description) if self.description else None,
+                             validators=field_validators,
+                             required=False,
+                             )

+ 15 - 0
misago/models/settingsgroupmodel.py

@@ -0,0 +1,15 @@
+from django.db import models
+
+class SettingsGroup(models.Model):
+    key = models.CharField(max_length=255, unique=True)
+    name = models.CharField(max_length=255)
+    description = models.TextField(null=True, blank=True)
+
+    class Meta:
+        app_label = 'misago'
+
+    def is_active(self, active_group):
+        try:
+            return self.pk == active_group.pk
+        except AttributeError:
+            return False

+ 0 - 1
misago/notrefactored/forms/__init__.py

@@ -1,5 +1,4 @@
 from django import forms
 from django import forms
-from django.utils.html import conditional_escape, mark_safe
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from mptt.forms import TreeNodeChoiceField
 from mptt.forms import TreeNodeChoiceField
 from misago.forms.layouts import *
 from misago.forms.layouts import *

+ 11 - 0
misago/notrefactored/utils/slugify.py

@@ -0,0 +1,11 @@
+import django.template.defaultfilters
+try:
+    from unidecode import unidecode
+    use_unidecode = True
+except ImportError:
+    use_unidecode = False
+
+def slugify(string):
+    if use_unidecode:
+        string = unidecode(string)
+    return django.template.defaultfilters.slugify(string)

+ 15 - 15
misago/settings_base.py

@@ -94,22 +94,22 @@ JINJA2_EXTENSIONS = (
 
 
 # List of application middlewares
 # List of application middlewares
 MIDDLEWARE_CLASSES = (
 MIDDLEWARE_CLASSES = (
-    'misago.stopwatch.middleware.StopwatchMiddleware',
+    #'misago.stopwatch.middleware.StopwatchMiddleware',
     'debug_toolbar.middleware.DebugToolbarMiddleware',
     'debug_toolbar.middleware.DebugToolbarMiddleware',
-    'misago.heartbeat.middleware.HeartbeatMiddleware',
-    'misago.cookie_jar.middleware.CookieJarMiddleware',
-    'misago.settings.middleware.SettingsMiddleware',
-    'misago.monitor.middleware.MonitorMiddleware',
-    'misago.themes.middleware.ThemeMiddleware',
-    'misago.firewalls.middleware.FirewallMiddleware',
-    'misago.crawlers.middleware.DetectCrawlerMiddleware',
-    'misago.sessions.middleware.SessionMiddleware',
-    'misago.bruteforce.middleware.JamMiddleware',
-    'misago.csrf.middleware.CSRFMiddleware',
-    'misago.banning.middleware.BanningMiddleware',
-    'misago.messages.middleware.MessagesMiddleware',
-    'misago.users.middleware.UserMiddleware',
-    'misago.acl.middleware.ACLMiddleware',
+    #'misago.heartbeat.middleware.HeartbeatMiddleware',
+    #'misago.cookie_jar.middleware.CookieJarMiddleware',
+    'misago.middleware.settings.SettingsMiddleware',
+    'misago.middleware.monitor.MonitorMiddleware',
+    #'misago.themes.middleware.ThemeMiddleware',
+    #'misago.firewalls.middleware.FirewallMiddleware',
+    #'misago.crawlers.middleware.DetectCrawlerMiddleware',
+    #'misago.sessions.middleware.SessionMiddleware',
+    #'misago.bruteforce.middleware.JamMiddleware',
+    #'misago.csrf.middleware.CSRFMiddleware',
+    #'misago.banning.middleware.BanningMiddleware',
+    #'misago.messages.middleware.MessagesMiddleware',
+    #'misago.users.middleware.UserMiddleware',
+    #'misago.acl.middleware.ACLMiddleware',
     'django.middleware.common.CommonMiddleware',
     'django.middleware.common.CommonMiddleware',
 )
 )
 
 

+ 0 - 0
misago/shared/__init__.py


+ 0 - 0
misago/templatetags/__init__.py


+ 0 - 0
misago/utils/__init__.py


+ 46 - 0
misago/utils/avatars.py

@@ -0,0 +1,46 @@
+from django.conf import settings
+try:
+    from PIL import Image
+    has_pil = True
+except ImportError:
+    has_pil = False
+
+avatar_sizes = {}
+def avatar_size(size):
+    if not has_pil:
+        return None
+    try:
+        return avatar_sizes[size]
+    except KeyError:
+        avatar_sizes[size] = None
+        for i in settings.AVATAR_SIZES[1:]:
+            if size <= i:
+                avatar_sizes[size] = i
+    return avatar_sizes[size]
+
+
+def resizeimage(image, size, target, info=None, format=None):
+    if isinstance(image, basestring):
+        image = Image.open(image)
+    if not info:
+        info = image.info
+    if not format:
+        format = image.format
+    if format == "GIF":
+        if 'transparency' in info:
+            image = image.resize((size, size), Image.ANTIALIAS)
+            image.save(target, image.format, transparency=info['transparency'])
+        else:
+            image = image.convert("RGB")
+            image = image.resize((size, size), Image.ANTIALIAS)
+            image = image.convert('P', palette=Image.ADAPTIVE)
+            image.save(target, image.format)
+    if format == "PNG":
+        image = image.resize((size, size), Image.ANTIALIAS)
+        image.save(target, quality=95)
+    if format == "JPEG":
+        image = image.convert("RGB")
+        image = image.resize((size, size), Image.ANTIALIAS)
+        image = image.convert('P', palette=Image.ADAPTIVE)
+        image = image.convert("RGB", dither=None)
+        image.save(target, image.format, quality=95)

+ 108 - 0
misago/utils/datesformats.py

@@ -0,0 +1,108 @@
+from datetime import datetime, timedelta
+from django.utils.dateformat import format, time_format
+from django.utils.formats import get_format
+from django.utils.timezone import is_aware, localtime, utc
+from django.utils.translation import pgettext, ungettext, ugettext as _
+from misago.utils.strings import slugify
+
+# Build date formats
+formats = {
+    'DATE_FORMAT': '',
+    'DATETIME_FORMAT': '',
+    'TIME_FORMAT': '',
+    'YEAR_MONTH_FORMAT': '',
+    'MONTH_DAY_FORMAT': '',
+    'SHORT_DATE_FORMAT': '',
+    'SHORT_DATETIME_FORMAT': '',
+}
+
+for key in formats:
+    formats[key] = get_format(key).replace('P', 'g:i a')
+    
+def date(val, arg=""):
+    if not val:
+        return _("Never")
+    if not arg:
+        arg = formats['DATE_FORMAT']
+    elif arg in formats:
+        arg = formats[arg]
+    return format(localtime(val), arg)
+
+
+def reldate(val, arg=""):
+    if not val:
+        return _("Never")
+    now = datetime.now(utc if is_aware(val) else None)
+    local_now = localtime(now)
+    diff = now - val
+    local = localtime(val)
+
+    # Today check
+    if format(local, 'Y-z') == format(local_now, 'Y-z'):
+        return _("Today, %(hour)s") % {'hour': time_format(local, formats['TIME_FORMAT'])}
+        
+    # Yesteday check
+    yesterday = localtime(now - timedelta(days=1))
+    if format(local, 'Y-z') == format(yesterday, 'Y-z'):
+        return _("Yesterday, %(hour)s") % {'hour': time_format(local, formats['TIME_FORMAT'])}
+        
+    # Tomorrow Check
+    tomorrow = localtime(now + timedelta(days=1))
+    if format(local, 'Y-z') == format(tomorrow, 'Y-z'):
+        return _("Tomorrow, %(hour)s") % {'hour': time_format(local, formats['TIME_FORMAT'])}
+        
+    # Day of Week check
+    if format(local, 'D') != format(local_now, 'D') and diff.days > -7 and diff.days < 7:
+        return _("%(day)s, %(hour)s") % {'day': format(local, 'l'), 'hour': time_format(local, formats['TIME_FORMAT'])}
+
+    # Fallback to date      
+    return date(val, arg)
+
+
+def reltimesince(val, arg=""):
+    if not val:
+        return _("Never")
+    now = datetime.now(utc if is_aware(val) else None)
+    diff = now - val
+    local = localtime(val)
+
+    # Difference is greater than day for sure
+    if diff.days != 0:
+        return reldate(val, arg)
+
+    # Display specific time
+    if diff.seconds >= 0:
+        if diff.seconds <= 5:
+            return _("Just now")
+            
+        if diff.seconds <= 60:
+            return _("Minute ago")
+            
+        if diff.seconds < 3600:
+            minutes = int(math.floor(diff.seconds / 60.0))
+            return ungettext(
+                    "Minute ago",
+                    "%(minutes)s minutes ago",
+                minutes) % {'minutes': minutes}
+                
+        if diff.seconds < 10800:
+            hours = int(math.floor(diff.seconds / 3600.0))
+            minutes = (diff.seconds - (hours * 3600)) / 60
+            
+            if minutes > 0:
+                return ungettext(
+                    "Hour and %(minutes)s ago",
+                    "%(hours)s hours and %(minutes)s ago",
+                hours) % {'hours': hours, 'minutes': ungettext(
+                        "%(minutes)s minute",
+                        "%(minutes)s minutes",
+                    minutes) % {'minutes': minutes}}
+                return _("%(hours)s hours and %(minutes)s minutes ago") % {'hours': hours, 'minutes': minutes}
+                
+            return ungettext(
+                    "Hour ago",
+                    "%(hours)s hours ago",
+                hours) % {'hours': hours}
+
+    # Fallback to reldate
+    return reldate(val, arg)

+ 115 - 0
misago/utils/fixtures.py

@@ -0,0 +1,115 @@
+import base64
+from django.utils import timezone
+from django.utils.importlib import import_module
+from misago.models import MonitorItem, SettingsGroup, Setting
+from misago.utils.translation import get_msgid
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+def load_fixture(name):
+    """
+    Load fixture
+    """
+    try:
+        fixture = import_module(name)
+        fixture.load()
+        return True
+    except (ImportError, AttributeError):
+        return False
+    except Exception as e:
+        print 'Could not load fixtures from %s:\n%s' % (name, e)
+        return False
+
+
+def update_fixture(name):
+    """
+    If fixture module contains update function, use it to update fixture
+    """
+    try:
+        fixture = import_module(name)
+        fixture.update()
+        return True
+    except (ImportError, AttributeError):
+        return False
+    except Exception as e:
+        print 'Could not update fixture from %s:\n%s' % (name, e)
+        return False
+
+
+def load_settings_group_fixture(group, fixture):
+    model_group = SettingsGroup(
+                                key=group,
+                                name=get_msgid(fixture['name']),
+                                description=get_msgid(fixture.get('description'))
+                                )
+    model_group.save(force_insert=True)
+    fixture = fixture.get('settings', ())
+    position = 0
+    for setting in fixture:
+        value = setting[1].get('value')
+        value_default = setting[1].get('default')
+        # Convert boolean True and False to 1 and 0, otherwhise it wont work
+        if setting[1].get('type') == 'boolean':
+            value = 1 if value else 0
+            value_default = 1 if value_default else 0
+        # Convert array value to string
+        if setting[1].get('type') == 'array':
+            value = ','.join(value) if value else ''
+            value_default = ','.join(value_default) if value_default else ''
+        # Store setting in database
+        model_setting = Setting(
+                                setting=setting[0],
+                                group=model_group,
+                                value=value,
+                                value_default=value_default,
+                                normalize_to=setting[1].get('type'),
+                                field=setting[1].get('input'),
+                                extra=base64.encodestring(pickle.dumps(setting[1].get('extra', {}), pickle.HIGHEST_PROTOCOL)),
+                                position=position,
+                                separator=get_msgid(setting[1].get('separator')),
+                                name=get_msgid(setting[1].get('name')),
+                                description=get_msgid(setting[1].get('description')),
+                                )
+        model_setting.save(force_insert=True)
+        position += 1
+
+
+def update_settings_group_fixture(group, fixture):
+    try:
+        model_group = SettingsGroup.objects.get(key=group)
+        settings = {}
+        for setting in model_group.setting_set.all():
+            settings[setting.pk] = setting.value
+        model_group.delete()
+        load_settings_group_fixture(group, fixture)
+
+        for setting in settings:
+            try:
+                new_setting = Setting.objects.get(pk=setting)
+                new_setting.value = settings[setting]
+                new_setting.save(force_update=True)
+            except Setting.DoesNotExist:
+                pass
+    except SettingsGroup.DoesNotExist:
+        load_settings_group_fixture(group, fixture)
+
+
+def load_settings_fixture(fixture):
+    for group in fixture:
+        load_settings_group_fixture(group[0], group[1])
+
+
+def update_settings_fixture(fixture):
+    for group in fixture:
+        update_settings_group_fixture(group[0], group[1])
+
+
+def load_monitor_fixture(fixture):
+    for id in fixture.keys():
+        item = MonitorItem.objects.create(
+                                          id=id,
+                                          value=fixture[id],
+                                          updated=timezone.now()
+                                          )

+ 30 - 0
misago/utils/pagination.py

@@ -0,0 +1,30 @@
+import math
+
+def make_pagination(page, total, max):
+    pagination = {'start': 0, 'stop': 0, 'prev':-1, 'next':-1}
+    page = int(page)
+    if page > 0:
+        pagination['start'] = (page - 1) * max
+
+    # Set page and total stat
+    pagination['page'] = int(pagination['start'] / max) + 1
+    pagination['total'] = int(math.ceil(total / float(max)))
+
+    # Fix too large offset
+    if pagination['start'] > total:
+        pagination['start'] = 0
+
+    # Allow prev/next?
+    if total > max:
+        if pagination['page'] > 1:
+            pagination['prev'] = pagination['page'] - 1
+        if pagination['page'] < pagination['total']:
+            pagination['next'] = pagination['page'] + 1
+
+    # Fix empty pagers
+    if not pagination['total']:
+        pagination['total'] = 1
+
+    # Set stop offset
+    pagination['stop'] = pagination['start'] + max
+    return pagination

+ 16 - 0
misago/utils/strings.py

@@ -0,0 +1,16 @@
+from django.template.defaultfilters import slugify
+from django.utils import crypto
+try:
+    from unidecode import unidecode
+    use_unidecode = True
+except ImportError:
+    use_unidecode = False
+
+def slugify(string):
+    if use_unidecode:
+        string = unidecode(string)
+    return django.template.defaultfilters.slugify(string)
+
+
+def get_random_string(length):
+    return crypto.get_random_string(length, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM")

+ 87 - 0
misago/utils/timezones.py

@@ -0,0 +1,87 @@
+import datetime
+from django.utils.translation import ugettext_lazy as _
+from django.utils import timezone
+import pytz
+
+raw_tz = (
+    ('Pacific/Apia', _('(UTC-13:00) Samoa'), _('(UTC-14:00) Samoa')),
+    ('Pacific/Midway', _('(UTC-11:00) Midway Islands, American Samoa')),
+    ('Pacific/Honolulu', _('(UTC-10:00) Cook Islands, Hawaii, Society Islands')),
+    ('America/Adak', _('(UTC-10:00) Aleutian Islands'), _('(UTC-09:00) Aleutian Islands')),
+    ('Pacific/Marquesas', _('(UTC-09:30) Marquesas Islands')),
+    ('Pacific/Gambier', _('(UTC-09:00) Gambier Islands')),
+    ('America/Anchorage', _('(UTC-09:00) Alaska Standard Time'), _('(UTC-08:00) Alaska Daylight Time')),
+    ('Pacific/Pitcairn', _('(UTC-08:00) Pitcairn Islands')),
+    ('America/Los_Angeles', _('(UTC-08:00) Pacific Time (Canada and US)'), _('(UTC-07:00) Pacific Time (Canada and US)')),
+    ('America/Santa_Isabel', _('(UTC-08:00) Baja California'), _('(UTC-07:00) Baja California')),
+    ('America/Phoenix', _('(UTC-07:00) Mountain Standard Time (No DST)')),
+    ('America/Hermosillo', _('(UTC-07:00) Sonora')),
+    ('America/Denver', _('(UTC-07:00) Mountain Standard Time'), _('(UTC-06:00) Mountain Summer Time')),
+    ('America/Chihuahua', _('(UTC-07:00) Baja California Sur, Chihuahua, Nayarit, Sinaloa'), _('(UTC-06:00) Baja California Sur, Chihuahua, Nayarit, Sinaloa')),
+    ('America/Costa_Rica', _('(UTC-06:00) Costa Rica, El Salvador, Galapagos, Guatemala, Managua')),
+    ('America/Chicago', _('(UTC-06:00) Central Standard Time'), _('(UTC-05:00) Central Daylight Time')),
+    ('America/Mexico_City', _('(UTC-06:00) Mexican Central Zone'), _('(UTC-05:00) Mexican Central Zone')),
+    ('America/Panama', _('(UTC-05:00) Bogota, Cayman, Guayaquil, Jamaica, Lima, Panama')),
+    ('America/New_York', _('(UTC-05:00) Eastern Standard Time'), _('(UTC-04:00) Eastern Daylight Time')),
+    ('America/Caracas', _('(UTC-04:30) Caracas')),
+    ('America/Puerto_Rico', _('(UTC-04:00) Barbados, Dominica, Puerto Rico, Santo Domingo')),
+    ('America/Santiago', _('(UTC-04:00) Bermuda, Campo Grande, Goose Bay, Santiago, Thule'), _('(UTC-03:00) Bermuda, Campo Grande, Goose Bay, Santiago, Thule')),
+    ('America/St_Johns', _('(UTC-03:30) Newfoundland Time')),
+    ('America/Argentina/La_Rioja', _('(UTC-03:00) San Juan, San Luis, Santa Cruz')),
+    ('America/Sao_Paulo', _('(UTC-03:00) Buenos Aires, Godthab, Sao Paulo, Montevideo'), _('(UTC-02:00) Buenos Aires, Godthab, Sao Paulo, Montevideo')),
+    ('America/Noronha', _('(UTC-02:00) Atlantic islands')),
+    ('Atlantic/Cape_Verde', _('(UTC-01:00) Cape Verde Time')),
+    ('Atlantic/Azores', _('(UTC-01:00) Azores, Scoresbysund'), _('(UTC) Azores, Scoresbysund')),
+    ('utc', _('(UTC) Coordinated Universal Time')),
+    ('Africa/Dakar', _('(UTC) Dakar, Rabat')),
+    ('Europe/Lisbon', _('(UTC) Western European Time'), _('(UTC+01:00) Western European Summer Time')),
+    ('Africa/Algiers', _('(UTC+01:00) West Africa Time')),
+    ('Europe/Zurich', _('(UTC+01:00) Central European Time'), _('(UTC+02:00) Central European Summer Time')),
+    ('Africa/Cairo', _('(UTC+02:00) Central Africa Time')),
+    ('Europe/Athens', _('(UTC+02:00) Eastern European Time'), _('(UTC+03:00) Eastern European Summer Time')),
+    ('Asia/Qatar', _('(UTC+03:00) East Africa Time')),
+    ('Europe/Minsk', _('(UTC+03:00) Further-eastern European Time')),
+    ('Asia/Tehran', _('(UTC+3:30) Iran Time'), _('(UTC+4:30) Iran Time')),
+    ('Europe/Moscow', _('(UTC+4:00) Moscow Standard Time, Georgian Time')),
+    ('Asia/Dubai', _('(UTC+04:00) United Arab Emirates Standard Time')),
+    ('Asia/Baku', _('(UTC+05:00) Baku, Yerevan'), _('(UTC+06:00) Baku, Yerevan')),
+    ('Asia/Kabul', _('(UTC+04:30) Afghanistan Standard Time')),
+    ('Asia/Karachi', _('(UTC+05:00) Ashgabat, Dushanbe, Karachi, Maldives, Tashkent')),
+    ('Asia/Kolkata', _('(UTC+05:30) Colombo, Kolkata')),
+    ('Asia/Kathmandu', _('(UTC+05:45) Kathmandu')),
+    ('Asia/Almaty', _('(UTC+06:00) Astana, Bishkek, Dhaka, Thimphu, Yekaterinburg')),
+    ('Asia/Rangoon', _('(UTC+06:30) Yangon, Cocos Islands')),
+    ('Asia/Bangkok', _('(UTC+07:00) Bangkok, Ho Chi Minh, Jakarta, Novosibirsk')),
+    ('Asia/Taipei', _('(UTC+08:00) Beijing, Hong Kong, Kuala Lumpur, Singapore, Taipei')),
+    ('Australia/Perth', _('(UTC+08:00) Australian Western Standard Time')),
+    ('Australia/Eucla', _('(UTC+08:45) Eucla Area')),
+    ('Asia/Tokyo', _('(UTC+09:00) Tokyo, Seoul, Irkutsk, Pyongyang')),
+    ('Australia/Darwin', _('(UTC+09:30) Australian Central Standard Time')),
+    ('Australia/Adelaide', _('(UTC+09:30) Australian Central Standard Time')),
+    ('Australia/Melbourne', _('(UTC+10:00) Australian Eastern Standard Time'), _('(UTC+11:00) Australian Eastern Summer Time')),
+    ('Australia/Lord_Howe', _('(UTC+10:30) Lord Howe Island'), _('(UTC+11:00) Lord Howe Island')),
+    ('Pacific/Guadalcanal', _('(UTC+11:00) Guadalcanal, Honiara, Noumea, Vladivostok')),
+    ('Pacific/Norfolk', _('(UTC+11:30) Norfolk Island')),
+    ('Pacific/Wake', _('(UTC+12:00) Kamchatka, Marshall Islands')),
+    ('Pacific/Auckland', _('(UTC+12:00) Auckland, Fiji'), _('(UTC+13:00) Auckland, Fiji')),
+    ('Pacific/Chatham', _('(UTC+12:45) Chatham Islands'), _('(UTC+13:45) Chatham Islands')),
+    ('Pacific/Enderbury', _('(UTC+13:00) Phoenix Islands')),
+    ('Pacific/Kiritimati', _('(UTC+14:00) Nuku\'alofa')),
+)
+
+def tzlist():
+    """
+    Generate user-friendly timezone list for selects
+    """
+    ready_list = []
+    utc_dt = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
+    for tz in raw_tz:
+        if len(tz) == 3:
+            tzinfo = pytz.timezone(tz[0])
+            if utc_dt.astimezone(tzinfo).dst().seconds > 0:
+                ready_list.append((tz[0], tz[2]))
+            else:
+                ready_list.append((tz[0], tz[1]))
+        else:
+           ready_list.append((tz[0], tz[1]))
+    return tuple(ready_list)

+ 20 - 0
misago/utils/translation.py

@@ -0,0 +1,20 @@
+from django.utils import translation
+
+def ugettext_lazy(string):
+    """
+    Custom wrapper that preserves untranslated message on lazy translation string object
+    """
+    t = translation.ugettext_lazy(string)
+    t.message = string
+    return t
+
+
+def get_msgid(gettext):
+    """
+    Function for extracting untranslated message from lazy translation string object
+    made trough ugettext_lazy
+    """
+    try:
+        return gettext.message
+    except AttributeError:
+        return None

+ 0 - 0
misago/validators/__init__.py