diff --git a/.gitattributes b/.gitattributes index 212566614..07bd8e8a8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,24 @@ -* text=auto \ No newline at end of file +# Git will handle the files in whatever way it thinks is best +* text=auto + +# Always convert line endings to LF on checkout +*.py text eol=lf +*.coffe text eol=lf +*.js text eol=lf +*.scss text eol=lf +*.css text eol=lf +*.html text eol=lf +*.md text eol=lf +*.txt text eol=lf +*.yml text eol=lf +*.po text eol=lf + +# Files that are truly binary and should not be modified +*.png binary +*.jpg binary +*.ico binary +*.eot binary +*.svg binary +*.ttf binary +*.woff binary +*.mo binary \ No newline at end of file diff --git a/.gitignore b/.gitignore index 22843b751..652e71daa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +# Spirit +spirit/whoosh_index/ +db.sqlite3 +static/ +media/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -34,10 +40,11 @@ htmlcov/ nosetests.xml coverage.xml -# Mr Developer +# Mr Developer / eclipse pydev .mr.developer.cfg .project .pydevproject +.settings # Rope .ropeproject @@ -55,4 +62,4 @@ docs/_build/ # Vim *~ *.swp -*.swo \ No newline at end of file +*.swo diff --git a/.travis.yml b/.travis.yml index b0972e79b..0fa1ff448 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,10 @@ python: install: - pip install -r requirements.txt --use-mirrors - - pip install coveralls pep8 flake8 + - pip install coveralls pep8==1.5.7 flake8 script: - - pep8 --max-line-length=120 --exclude='migrations,tests' . - - flake8 --select=F401 ./spirit - - coverage run --source=. run_tests.py + - flake8 --exit-zero + - ./runtests.py example + - coverage run --source=. runtests.py after_success: - coveralls diff --git a/README.md b/README.md index 6c16ec712..c8b278115 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Check out the [example](https://github.com/nitely/Spirit/tree/master/example) pr In short: -Add `url(r'^', include('spirit.urls', namespace="spirit", app_name="spirit")),` to your *urls.py* +Add `url(r'^', include('spirit.urls')),` to your *urls.py* Add `from spirit.settings import *` to the top of your *settings.py* file, otherwise you will have to setup all django's related constants (Installed_apps, Middlewares, Login_url, etc) @@ -80,6 +80,13 @@ Feel free to check out the source code and submit pull requests. You may also report any bug or propose new features in the [issues tracker](https://github.com/nitely/Spirit/issues) +## Testing + +The `runtests.py` script enable you to run the test suite of spirit. + +- Type `./runtests.py` to run the test suite using the settings from the `tests` folder. +- Type `./runtests.py example` to run the test suite using the settings from the `example` folder. + ## Copyright / License Copyright 2014 [Esteban Castro Borsani](https://github.com/nitely). diff --git a/TODO b/TODO index a19077e03..3528f0123 100644 --- a/TODO +++ b/TODO @@ -26,6 +26,7 @@ * >> add @username on reply * >> allow mods to create topics on closed categories * >> order profile topics by creation date +* >> accessing custom user attr makes an extra query Template @@ -34,8 +35,9 @@ Template * admin: add nav to detail templates * Notifications: show all/unread link * local store comment publish/update +* topic: like/dislike icon disappears when clicked (js) Readme * add note about translations and fuzziness -* update: rebuild_index (search) command \ No newline at end of file +* update: rebuild_index (search) command diff --git a/example/README.md b/example/README.md new file mode 100644 index 000000000..729accb2e --- /dev/null +++ b/example/README.md @@ -0,0 +1,26 @@ +#Running the example application + +Assuming you use virtualenv, follow these steps to download and run the +Spirit example application in this directory: + + + $ git clone https://github.com/nitely/Spirit.git + $ cd Spirit + $ virtualenv venv + $ source ./venv/bin/activate + $ pip install . + $ cd example + $ python manage.py migrate + $ python manage.py createcachetable spirit_cache + $ export SECRET_KEY="My dev box" + $ python manage.py runserver + +> **Note:** +> +> When running on production, remember to set the SECRET_KEY environment +> variable or use your own setting file to set it. +> +> If you want to give it a quick spin, run `$ python manage.py runserver --settings=project.settings.dev` + +You should then be able to open your browser on http://127.0.0.1:8000 and +see the Spirit homepage. diff --git a/example/manage.py b/example/manage.py new file mode 100755 index 000000000..1d58ef1c2 --- /dev/null +++ b/example/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings.prod") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/spirit/tests/__init__.py b/example/project/__init__.py similarity index 100% rename from spirit/tests/__init__.py rename to example/project/__init__.py diff --git a/example/project/settings/__init__.py b/example/project/settings/__init__.py new file mode 100644 index 000000000..bb550ad52 --- /dev/null +++ b/example/project/settings/__init__.py @@ -0,0 +1 @@ +__author__ = 'Admin' diff --git a/example/settings.py b/example/project/settings/base.py similarity index 68% rename from example/settings.py rename to example/project/settings/base.py index 8dca9bbba..0948e6ab2 100644 --- a/example/settings.py +++ b/example/project/settings/base.py @@ -1,42 +1,23 @@ # -*- coding: utf-8 -*- """ -Django settings for test2 project. - -For more information on this file, see -https://docs.djangoproject.com/en/1.6/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.6/ref/settings/ +Django settings for running the example of spirit app """ from __future__ import unicode_literals import os - - -# You may override spirit settings below... +import sys from spirit.settings import * +# You may override spirit settings below... # Build paths inside the project like this: os.path.join(BASE_DIR, ...) - -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'change-me' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False - -TEMPLATE_DEBUG = False - -ALLOWED_HOSTS = [] - - # Application definition # Extend the Spirit installed apps (notice the plus sign) @@ -44,7 +25,6 @@ INSTALLED_APPS += ( # 'my_app1', # 'my_app2', - 'debug_toolbar', ) # same here, check out the spirit.settings.py @@ -67,20 +47,9 @@ }) -ROOT_URLCONF = 'example.urls' - -WSGI_APPLICATION = 'example.wsgi.application' +ROOT_URLCONF = 'project.urls' - -# Database -# https://docs.djangoproject.com/en/1.6/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} +WSGI_APPLICATION = 'project.wsgi.application' # Internationalization # https://docs.djangoproject.com/en/1.6/topics/i18n/ @@ -95,7 +64,6 @@ USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.6/howto/static-files/ @@ -133,10 +101,3 @@ }, } } - -try: - # devs must create this file to override settings - # local_settings_sample.py is provided - from .local_settings import * -except ImportError: - pass diff --git a/example/project/settings/dev.py b/example/project/settings/dev.py new file mode 100644 index 000000000..28686b752 --- /dev/null +++ b/example/project/settings/dev.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +# THIS IS FOR DEVELOPMENT ENVIRONMENT +# DO NOT USE IT IN PRODUCTION + +# Create your own dev_local.py +# import * this module there and use it like this: +# python manage.py runserver --settings=project.settings.dev_local + +from __future__ import unicode_literals + +from .base import * + + +DEBUG = True + +TEMPLATE_DEBUG = True + +SECRET_KEY = "DEV" + +ALLOWED_HOSTS = ['127.0.0.1', ] + +INSTALLED_APPS += ( + 'debug_toolbar', +) + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + +CACHES.update({ + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + }, +}) + +PASSWORD_HASHERS = ( + 'django.contrib.auth.hashers.MD5PasswordHasher', +) + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/example/local_settings_sample_prod.py b/example/project/settings/prod.py similarity index 62% rename from example/local_settings_sample_prod.py rename to example/project/settings/prod.py index b155b3873..a92606af1 100644 --- a/example/local_settings_sample_prod.py +++ b/example/project/settings/prod.py @@ -2,8 +2,15 @@ # MINIMAL CONFIGURATION FOR PRODUCTION ENV +# Create your own prod_local.py +# import * this module there and use it like this: +# python manage.py runserver --settings=project.settings.prod_local + from __future__ import unicode_literals +from .base import * + + DEBUG = False TEMPLATE_DEBUG = False @@ -12,11 +19,18 @@ ADMINS = (('John', 'john@example.com'), ) # Secret key generator: https://djskgen.herokuapp.com/ -SECRET_KEY = '' +# You should set your key as an environ variable +SECRET_KEY = os.environ.get("SECRET_KEY", "") # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = ['.example.com', ] +# Extend the Spirit installed apps (notice the plus sign) +# Check out the .base.py file for more examples +INSTALLED_APPS += ( + # 'my_app1', +) + # https://docs.djangoproject.com/en/dev/ref/settings/#databases DATABASES = { 'default': { @@ -41,3 +55,11 @@ # Default language LANGUAGE_CODE = 'en' + +# Keep templates in memory +TEMPLATE_LOADERS = ( + ('django.template.loaders.cached.Loader', ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + )), +) diff --git a/example/project/settings/test.py b/example/project/settings/test.py new file mode 100644 index 000000000..8b03d991b --- /dev/null +++ b/example/project/settings/test.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +# THIS IS FOR DEVELOPMENT ENVIRONMENT +# DO NOT USE IT IN PRODUCTION + +# This is used to test settings and urls from example directory +# with `./runtests.py example` + +from __future__ import unicode_literals + +from .base import * + +SECRET_KEY = "TEST" + +INSTALLED_APPS += ( + 'tests', +) + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db_test.sqlite3'), + } +} + +ROOT_URLCONF = 'example.project.urls' + +PASSWORD_HASHERS = ( + 'django.contrib.auth.hashers.MD5PasswordHasher', +) diff --git a/example/urls.py b/example/project/urls.py similarity index 91% rename from example/urls.py rename to example/project/urls.py index 499baee2a..638e0613f 100644 --- a/example/urls.py +++ b/example/project/urls.py @@ -17,7 +17,7 @@ # url(r'^$', 'example.views.home', name='home'), # url(r'^example/', include('example.foo.urls')), - url(r'^', include('spirit.urls', namespace="spirit", app_name="spirit")), + url(r'^', include('spirit.urls')), # Uncomment the admin/doc line below to enable admin documentation: # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), diff --git a/example/wsgi.py b/example/project/wsgi.py similarity index 100% rename from example/wsgi.py rename to example/project/wsgi.py diff --git a/example/settings_test_runner.py b/example/settings_test_runner.py deleted file mode 100644 index e27a14060..000000000 --- a/example/settings_test_runner.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals - -from .settings import * -from .local_settings_sample_dev import * - -INSTALLED_APPS += ( - 'spirit.tests', -) diff --git a/requirements.txt b/requirements.txt index db8925998..425e64d14 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ whoosh>=2.5,<2.6 mistune>=0.3.1,<0.4 Pillow>=2.6,<2.7 django-infinite-scroll-pagination>=0.1.3,<0.2 -django-djconfig>=0.1.7,<0.2 +django-djconfig>=0.2.0,<0.3 django-debug-toolbar -pytz \ No newline at end of file +pytz diff --git a/run_tests.py b/run_tests.py deleted file mode 100755 index d6c437eff..000000000 --- a/run_tests.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import os -import sys - -os.environ['DJANGO_SETTINGS_MODULE'] = 'example.settings_test_runner' - -import django -from django.test.runner import DiscoverRunner - - -def run_tests(): - test_runner = DiscoverRunner() - failures = test_runner.run_tests(["spirit", ]) - sys.exit(failures) - - -if __name__ == "__main__": - django.setup() - run_tests() diff --git a/runtests.py b/runtests.py new file mode 100755 index 000000000..645028b4c --- /dev/null +++ b/runtests.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +import os +import sys +import logging + +import django +from django.test.runner import DiscoverRunner + + +EXAMPLE = 'example' in sys.argv + +if EXAMPLE: + # Run tests with example settings + os.environ['DJANGO_SETTINGS_MODULE'] = 'example.project.settings.test' # pragma: no cover +else: + # Run tests with tests settings + os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' + + +def log_warnings(): + logger = logging.getLogger('py.warnings') + handler = logging.StreamHandler() + logger.addHandler(handler) + + +def run_tests(): + sys.stdout.write("\nRunning spirit test suite, using settings %(settings)r\n\n" + % {"settings": os.environ['DJANGO_SETTINGS_MODULE'], }) + test_runner = DiscoverRunner() + failures = test_runner.run_tests(["tests", ]) + sys.exit(failures) + + +def start(): + django.setup() + log_warnings() + run_tests() + + +if __name__ == "__main__": + start() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..7d1118b75 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[flake8] +exclude=.git,migrations/,*settings*.py +ignore=E123,E128,E265,E501,W601 +max-line-length = 119 diff --git a/setup.py b/setup.py index 4744bdabf..54b077fa5 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ long_description=README, url='http://spirit-project.com/', packages=find_packages(exclude=['example', ]), - test_suite="run_tests.run_tests", + test_suite="runtests.start", include_package_data=True, zip_safe=False, install_requires=REQUIREMENTS, diff --git a/spirit/apps.py b/spirit/apps.py index 4ea41da7f..4bf984222 100644 --- a/spirit/apps.py +++ b/spirit/apps.py @@ -9,3 +9,16 @@ class SpiritConfig(AppConfig): name = 'spirit' verbose_name = "Spirit" + + def ready(self): + self.register_config() + self.register_signals() + + def register_config(self): + import djconfig + from spirit.forms.admin import BasicConfigForm + + djconfig.register(BasicConfigForm) + + def register_signals(self): + from spirit.signals import handlers diff --git a/spirit/forms/admin.py b/spirit/forms/admin.py index cff9f03d4..f0c25e0c4 100644 --- a/spirit/forms/admin.py +++ b/spirit/forms/admin.py @@ -31,7 +31,7 @@ class Meta: def __init__(self, *args, **kwargs): super(CategoryForm, self).__init__(*args, **kwargs) - queryset = Category.objects.for_parent() + queryset = Category.objects.visible().parents() if self.instance.pk: queryset = queryset.exclude(pk=self.instance.pk) @@ -73,3 +73,5 @@ class BasicConfigForm(ConfigForm): template_footer = forms.CharField(initial="", label=_("footer snippet"), required=False, widget=forms.Textarea(attrs={'rows': 2, }), help_text=_("This gets rendered just before the footer in your template.")) + comments_per_page = forms.IntegerField(initial=20, label=_("comments per page"), min_value=1, max_value=100) + topics_per_page = forms.IntegerField(initial=20, label=_("topics per page"), min_value=1, max_value=100) diff --git a/spirit/forms/comment_flag.py b/spirit/forms/comment_flag.py index b494662d0..d2c8b1fd3 100644 --- a/spirit/forms/comment_flag.py +++ b/spirit/forms/comment_flag.py @@ -38,11 +38,10 @@ def save(self, commit=True): self.instance.user = self.user self.instance.comment = self.comment - # TODO: use update_or_create on django 1.7 try: - CommentFlag.objects.create(comment=self.comment) + CommentFlag.objects.update_or_create(comment=self.comment, + defaults={'date': timezone.now(), }) except IntegrityError: - CommentFlag.objects.filter(comment=self.comment)\ - .update(date=timezone.now()) + pass return super(FlagForm, self).save(commit) diff --git a/spirit/forms/search.py b/spirit/forms/search.py index d7c4a15fb..891d00fa6 100644 --- a/spirit/forms/search.py +++ b/spirit/forms/search.py @@ -39,7 +39,7 @@ def search(self): class AdvancedSearchForm(BaseSearchForm): - category = forms.ModelMultipleChoiceField(queryset=Category.objects.for_public(), + category = forms.ModelMultipleChoiceField(queryset=Category.objects.visible(), required=False, label=_('Filter by'), widget=forms.CheckboxSelectMultiple) diff --git a/spirit/forms/topic.py b/spirit/forms/topic.py index 767be743b..7bbc2d30a 100644 --- a/spirit/forms/topic.py +++ b/spirit/forms/topic.py @@ -20,8 +20,7 @@ class Meta: def __init__(self, user, *args, **kwargs): super(TopicForm, self).__init__(*args, **kwargs) self.user = user - # TODO: add custom Prefetch object to filter closed sub-categories, on Django 1.7 - self.fields['category'] = NestedModelChoiceField(queryset=Category.objects.for_public_open(), + self.fields['category'] = NestedModelChoiceField(queryset=Category.objects.visible().opened(), related_name='category_set', parent_field='parent_id', label_field='title', @@ -31,15 +30,6 @@ def __init__(self, user, *args, **kwargs): if self.instance.pk and not user.is_moderator: del self.fields['category'] - def clean_category(self): - # TODO: remove this on django 1.7 - category = self.cleaned_data['category'] - - if category.is_closed or category.is_removed: - raise forms.ValidationError(_("The chosen category is closed")) - - return category - def save(self, commit=True): if not self.instance.pk: self.instance.user = self.user diff --git a/spirit/forms/topic_notification.py b/spirit/forms/topic_notification.py index 1dafa56d9..cfc40b865 100644 --- a/spirit/forms/topic_notification.py +++ b/spirit/forms/topic_notification.py @@ -10,7 +10,7 @@ class NotificationForm(forms.ModelForm): - is_active = forms.BooleanField(widget=forms.HiddenInput(), required=False) + is_active = forms.BooleanField(widget=forms.HiddenInput(), initial=True, required=False) class Meta: model = TopicNotification diff --git a/spirit/forms/topic_poll.py b/spirit/forms/topic_poll.py index 8194dbe28..d2cc4ee1c 100644 --- a/spirit/forms/topic_poll.py +++ b/spirit/forms/topic_poll.py @@ -6,6 +6,7 @@ from django.forms.models import inlineformset_factory, BaseInlineFormSet from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_text +from django.core.exceptions import ValidationError from spirit.models.topic_poll import TopicPollChoice, TopicPoll, TopicPollVote @@ -24,7 +25,7 @@ def clean_choice_limit(self): choice_limit = self.cleaned_data['choice_limit'] if choice_limit < 1: - raise forms.ValidationError(_("This must be greater than zero")) + raise forms.ValidationError(_("Choice's limit must be greater than zero")) return choice_limit @@ -35,33 +36,69 @@ def save(self, commit=True): return super(TopicPollForm, self).save(commit) +class TopicPollChoiceForm(forms.ModelForm): + + class Meta: + model = TopicPollChoice + fields = ['description', ] + + def is_filled(self): + description = self.cleaned_data.get('description') + is_marked_as_delete = self.cleaned_data.get('DELETE', False) + + if description and not is_marked_as_delete: + return True + + return False + + class TopicPollChoiceInlineFormSet(BaseInlineFormSet): def __init__(self, can_delete=None, *args, **kwargs): super(TopicPollChoiceInlineFormSet, self).__init__(*args, **kwargs) + self._is_filled_cache = None if can_delete is not None: # Adds the *delete* checkbox or not self.can_delete = can_delete - def is_filled(self): + def _is_filled(self): if self.instance.pk: return True - for form in self.forms: - description = form.cleaned_data.get('description') - is_marked_as_delete = form.cleaned_data.get('DELETE', False) + return any([form.is_filled() for form in self.forms]) - if description and not is_marked_as_delete: - return True + def is_filled(self): + if self._is_filled_cache is None: + self._is_filled_cache = self._is_filled() - return False + return self._is_filled_cache + + def has_errors(self): + return any(self.errors) or self.non_form_errors() + + def clean(self): + # Stores in formset.non_field_errors + super(TopicPollChoiceInlineFormSet, self).clean() + + if not self.is_filled(): + return + + forms_filled = [form for form in self.forms if form.is_filled()] + + if len(forms_filled) < 2: + raise ValidationError(_("There must be 2 or more choices")) + + def save(self, commit=True): + if not self.is_filled(): + raise Exception("You should check and save if is_filled is True") + + return super(TopicPollChoiceInlineFormSet, self).save(commit=commit) -# TODO: use min_num and validate_min in Django 1.7 -TopicPollChoiceFormSet = inlineformset_factory(TopicPoll, TopicPollChoice, - formset=TopicPollChoiceInlineFormSet, fields=('description', ), - extra=2, max_num=20, validate_max=True) +TopicPollChoiceFormSet = inlineformset_factory(TopicPollForm._meta.model, TopicPollChoiceForm._meta.model, + form=TopicPollChoiceForm, formset=TopicPollChoiceInlineFormSet, + max_num=20, validate_max=True, extra=2) class TopicPollVoteManyForm(forms.Form): @@ -76,15 +113,17 @@ def __init__(self, user=None, poll=None, *args, **kwargs): self.poll = poll choices = TopicPollChoice.objects.filter(poll=poll) - if poll.choice_limit > 1: + if poll.is_multiple_choice: self.fields['choices'] = forms.ModelMultipleChoiceField(queryset=choices, + cache_choices=True, widget=forms.CheckboxSelectMultiple, label=_("Poll choices")) else: self.fields['choices'] = forms.ModelChoiceField(queryset=choices, - empty_label=None, + cache_choices=True, widget=forms.RadioSelect, - label=_("Poll choices")) + label=_("Poll choices"), + empty_label=None) self.fields['choices'].label_from_instance = lambda obj: smart_text(obj.description) @@ -94,7 +133,7 @@ def load_initial(self): if not selected_choices: return - if self.poll.choice_limit == 1: + if not self.poll.is_multiple_choice: selected_choices = selected_choices[0] self.initial = {'choices': selected_choices, } @@ -102,10 +141,10 @@ def load_initial(self): def clean_choices(self): choices = self.cleaned_data['choices'] - if self.poll.choice_limit > 1: - if len(choices) > self.poll.choice_limit: - raise forms.ValidationError(_("Too many selected choices. Limit is %s") - % self.poll.choice_limit) + if (self.poll.is_multiple_choice and + len(choices) > self.poll.choice_limit): + raise forms.ValidationError(_("Too many selected choices. Limit is %s") + % self.poll.choice_limit) return choices @@ -120,7 +159,7 @@ def clean(self): def save_m2m(self): choices = self.cleaned_data['choices'] - if self.poll.choice_limit == 1: + if not self.poll.is_multiple_choice: choices = [choices, ] TopicPollVote.objects.filter(user=self.user, choice__poll=self.poll)\ diff --git a/spirit/forms/user.py b/spirit/forms/user.py index d94c990ff..d1bea7bc2 100644 --- a/spirit/forms/user.py +++ b/spirit/forms/user.py @@ -108,8 +108,8 @@ def clean_email(self): except User.DoesNotExist: raise forms.ValidationError(_("The provided email does not exists.")) - if self.user.last_ip: - raise forms.ValidationError(_("This account was activated.")) + if self.user.is_verified: + raise forms.ValidationError(_("This account is verified, try logging-in.")) return email diff --git a/spirit/managers/category.py b/spirit/managers/category.py index 5afea9407..47d054793 100644 --- a/spirit/managers/category.py +++ b/spirit/managers/category.py @@ -2,31 +2,31 @@ from __future__ import unicode_literals -from django.db.models import Manager +from django.db import models from django.db.models import Q -from django.shortcuts import get_object_or_404 -class CategoryManager(Manager): +class CategoryQuerySet(models.QuerySet): - def for_public(self): + def unremoved(self): return self.filter(Q(parent=None) | Q(parent__is_removed=False), - is_removed=False, - is_private=False) + is_removed=False) - def for_public_open(self): - return self.for_public()\ - .filter(Q(parent=None) | Q(parent__is_closed=False), - is_closed=False) + def public(self): + return self.filter(is_private=False) - def for_parent(self, parent=None): - if parent and parent.is_subcategory: + def visible(self): + return self.unremoved().public() + + def opened(self): + return self.filter(Q(parent=None) | Q(parent__is_closed=False), + is_closed=False) + + def parents(self): + return self.filter(parent=None) + + def children(self, parent): + if parent.is_subcategory: return self.none() - else: - return self.filter(parent=parent, - is_removed=False, - is_private=False) - - def get_public_or_404(self, pk): - return get_object_or_404(self.for_public(), - pk=pk) + + return self.filter(parent=parent) diff --git a/spirit/managers/comment.py b/spirit/managers/comment.py index c5fd52440..20efdb083 100644 --- a/spirit/managers/comment.py +++ b/spirit/managers/comment.py @@ -2,43 +2,53 @@ from __future__ import unicode_literals -from django.db.models import Manager +from django.db import models from django.shortcuts import get_object_or_404 -from django.db.models import Q +from django.db.models import Q, Prefetch +from ..models.comment_like import CommentLike -class CommentManager(Manager): - def _for_all(self): +class CommentQuerySet(models.QuerySet): + + def filter(self, *args, **kwargs): + # TODO: find a better way + return super(CommentQuerySet, self).filter(*args, **kwargs)\ + .select_related('user') + + def unremoved(self): + # TODO: remove action return self.filter(Q(topic__category__parent=None) | Q(topic__category__parent__is_removed=False), topic__category__is_removed=False, topic__is_removed=False, is_removed=False, - action=0)\ - .select_related('user') + action=0) - def for_all(self): - return self.filter(is_removed=False, action=0)\ - .select_related('user') + def public(self): + return self.filter(topic__category__is_private=False) + + def visible(self): + return self.unremoved().public() def for_topic(self, topic): - return self.filter(topic=topic)\ - .select_related('user') + return self.filter(topic=topic) + + def _access(self, user): + return self.filter(Q(topic__category__is_private=False) | Q(topic__topics_private__user=user)) - def for_public(self): - return self._for_all()\ - .filter(topic__category__is_private=False) + def with_likes(self, user): + if not user.is_authenticated(): + return self - def for_user_public(self, user): - return self.for_public()\ - .filter(user=user) + user_likes = CommentLike.objects.filter(user=user) + prefetch = Prefetch("comment_likes", queryset=user_likes, to_attr='likes') + return self.prefetch_related(prefetch) def for_access(self, user): - return self._for_all()\ - .filter(Q(topic__category__is_private=False) | Q(topic__topics_private__user=user)) + return self.unremoved()._access(user=user) def for_update_or_404(self, pk, user): if user.is_moderator: - return get_object_or_404(self, pk=pk) + return get_object_or_404(self._access(user=user), pk=pk) else: return get_object_or_404(self.for_access(user), user=user, pk=pk) diff --git a/spirit/managers/comment_like.py b/spirit/managers/comment_like.py deleted file mode 100644 index a72e3519f..000000000 --- a/spirit/managers/comment_like.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals - -from django.db.models import Manager - - -class CommentLikeManager(Manager): - - def for_user(self, user): - return self.filter(user=user)\ - .select_related('user', 'comment__user') diff --git a/spirit/managers/topic.py b/spirit/managers/topic.py index 0b05127b3..7ec3be142 100644 --- a/spirit/managers/topic.py +++ b/spirit/managers/topic.py @@ -2,60 +2,67 @@ from __future__ import unicode_literals -from django.db.models import Manager +from django.db import models from django.shortcuts import get_object_or_404 -from django.db.models import Q +from django.db.models import Q, Prefetch +from ..models.comment_bookmark import CommentBookmark -class TopicManager(Manager): - def _for_all(self): +class TopicQuerySet(models.QuerySet): + + def unremoved(self): return self.filter(Q(category__parent=None) | Q(category__parent__is_removed=False), category__is_removed=False, is_removed=False) - def for_public(self): - return self._for_all()\ - .filter(category__is_private=False) + def public(self): + return self.filter(category__is_private=False) + + def visible(self): + return self.unremoved().public() - def for_public_open(self): - return self.for_public()\ - .filter(is_closed=False) + def opened(self): + return self.filter(is_closed=False) def for_category(self, category): if category.is_subcategory: - return self.filter(category=category, - is_removed=False) - else: - return self.filter(Q(category=category) | Q(category__parent=category), - category__is_removed=False, - is_removed=False) + return self.filter(category=category) + + return self.filter(Q(category=category) | Q(category__parent=category)) + + def _access(self, user): + return self.filter(Q(category__is_private=False) | Q(topics_private__user=user)) + + def for_access(self, user): + return self.unremoved()._access(user=user) + + def for_unread(self, user): + return self.filter(topicunread__user=user, + topicunread__is_read=False) + + def with_bookmarks(self, user): + if not user.is_authenticated(): + return self + + user_bookmarks = CommentBookmark.objects\ + .filter(user=user)\ + .select_related('topic') + prefetch = Prefetch("commentbookmark_set", queryset=user_bookmarks, to_attr='bookmarks') + return self.prefetch_related(prefetch) def get_public_or_404(self, pk, user): if user.is_authenticated() and user.is_moderator: - return get_object_or_404(self + return get_object_or_404(self.public() .select_related('category__parent'), - pk=pk, - category__is_private=False) + pk=pk) else: - return get_object_or_404(self.for_public() + return get_object_or_404(self.visible() .select_related('category__parent'), pk=pk) def for_update_or_404(self, pk, user): if user.is_moderator: - return get_object_or_404(self, - pk=pk, - category__is_private=False) + return get_object_or_404(self.public(), pk=pk) else: - return get_object_or_404(self.for_public_open(), - pk=pk, - user=user) - - def for_access(self, user): - return self._for_all()\ - .filter(Q(category__is_private=False) | Q(topics_private__user=user)) - - def for_access_open(self, user): - return self.for_access(user)\ - .filter(is_closed=False) + return get_object_or_404(self.visible().opened(), pk=pk, user=user) diff --git a/spirit/managers/topic_notifications.py b/spirit/managers/topic_notifications.py index d44fa57c0..cbf0fb3e9 100644 --- a/spirit/managers/topic_notifications.py +++ b/spirit/managers/topic_notifications.py @@ -2,21 +2,26 @@ from __future__ import unicode_literals -from django.db.models import Manager +from django.db import models from django.db.models import Q -class TopicNotificationManager(Manager): +class TopicNotificationQuerySet(models.QuerySet): - def _for_all(self): + def unremoved(self): return self.filter(Q(topic__category__parent=None) | Q(topic__category__parent__is_removed=False), topic__category__is_removed=False, topic__is_removed=False) + def unread(self): + return self.filter(is_read=False) + + def _access(self, user): + return self.filter(Q(topic__category__is_private=False) | Q(topic__topics_private__user=user), + user=user) + def for_access(self, user): - return self._for_all()\ - .filter(Q(topic__category__is_private=False) | Q(topic__topics_private__user=user), - user=user)\ + return self.unremoved()._access(user=user)\ .exclude(comment=None) def read(self, user): diff --git a/spirit/managers/topic_private.py b/spirit/managers/topic_private.py index 4a6acb6c9..306579af0 100644 --- a/spirit/managers/topic_private.py +++ b/spirit/managers/topic_private.py @@ -2,22 +2,19 @@ from __future__ import unicode_literals -from django.db.models import Manager +from django.db import models from django.shortcuts import get_object_or_404 from django.db.models import Q -class TopicPrivateManager(Manager): +class TopicPrivateQuerySet(models.QuerySet): def for_delete_or_404(self, pk, user): - # User is the creator or wants to leave + # User is the creator *or* has access return get_object_or_404(self, Q(topic__user=user) | Q(user=user), pk=pk) def for_create_or_404(self, topic_id, user): - # User is creator and has access - return get_object_or_404(self, - topic_id=topic_id, - user=user, - topic__user=user) + # User is creator *and* has access + return get_object_or_404(self, topic_id=topic_id, user=user, topic__user=user) diff --git a/spirit/managers/topic_unread.py b/spirit/managers/topic_unread.py deleted file mode 100644 index 769cb46b7..000000000 --- a/spirit/managers/topic_unread.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals - -from django.db.models import Manager - - -class TopicUnreadManager(Manager): - - def for_user(self, user): - return self.filter(user=user, is_read=False, is_removed=False) - - def read(self, user, topic): - # returns updated rows count (int) - return self.filter(user=user, topic=topic)\ - .update(is_read=True) diff --git a/spirit/migrations/0001_initial.py b/spirit/migrations/0001_initial.py index f3bf7a935..99ecc2e61 100644 --- a/spirit/migrations/0001_initial.py +++ b/spirit/migrations/0001_initial.py @@ -16,6 +16,8 @@ class Migration(migrations.Migration): ('auth', '0001_initial'), ] + dependencies.extend(settings.ST_INITIAL_MIGRATION_DEPENDENCIES) + operations = [ migrations.CreateModel( name='User', diff --git a/spirit/migrations/0003_auto_20141220_1125.py b/spirit/migrations/0003_auto_20141220_1125.py new file mode 100644 index 000000000..b715cf8be --- /dev/null +++ b/spirit/migrations/0003_auto_20141220_1125.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import spirit.utils.models +import django.core.validators +import re + + +class Migration(migrations.Migration): + + dependencies = [ + ('spirit', '0002_auto_20140928_2347'), + ] + + operations = [ + migrations.AlterField( + model_name='category', + name='slug', + field=spirit.utils.models.AutoSlugField(blank=True, populate_from='title', db_index=False), + preserve_default=True, + ), + migrations.AlterField( + model_name='topic', + name='slug', + field=spirit.utils.models.AutoSlugField(blank=True, populate_from='title', db_index=False), + preserve_default=True, + ), + migrations.AlterField( + model_name='topic', + name='title', + field=models.CharField(max_length=255, verbose_name='title'), + preserve_default=True, + ), + migrations.AlterField( + model_name='user', + name='slug', + field=spirit.utils.models.AutoSlugField(blank=True, populate_from='username', db_index=False), + preserve_default=True, + ), + migrations.AlterField( + model_name='user', + name='timezone', + field=models.CharField(choices=[('Etc/GMT+12', '(GMT -12:00) Eniwetok, Kwajalein'), ('Etc/GMT+11', '(GMT -11:00) Midway Island, Samoa'), ('Etc/GMT+10', '(GMT -10:00) Hawaii'), ('Pacific/Marquesas', '(GMT -9:30) Marquesas Islands'), ('Etc/GMT+9', '(GMT -9:00) Alaska'), ('Etc/GMT+8', '(GMT -8:00) Pacific Time (US & Canada)'), ('Etc/GMT+7', '(GMT -7:00) Mountain Time (US & Canada)'), ('Etc/GMT+6', '(GMT -6:00) Central Time (US & Canada), Mexico City'), ('Etc/GMT+5', '(GMT -5:00) Eastern Time (US & Canada), Bogota, Lima'), ('America/Caracas', '(GMT -4:30) Venezuela'), ('Etc/GMT+4', '(GMT -4:00) Atlantic Time (Canada), Caracas, La Paz'), ('Etc/GMT+3', '(GMT -3:00) Brazil, Buenos Aires, Georgetown'), ('Etc/GMT+2', '(GMT -2:00) Mid-Atlantic'), ('Etc/GMT+1', '(GMT -1:00) Azores, Cape Verde Islands'), ('UTC', '(GMT) Western Europe Time, London, Lisbon, Casablanca'), ('Etc/GMT-1', '(GMT +1:00) Brussels, Copenhagen, Madrid, Paris'), ('Etc/GMT-2', '(GMT +2:00) Kaliningrad, South Africa'), ('Etc/GMT-3', '(GMT +3:00) Baghdad, Riyadh, Moscow, St. Petersburg'), ('Etc/GMT-4', '(GMT +4:00) Abu Dhabi, Muscat, Baku, Tbilisi'), ('Asia/Kabul', '(GMT +4:30) Afghanistan'), ('Etc/GMT-5', '(GMT +5:00) Ekaterinburg, Islamabad, Karachi, Tashkent'), ('Asia/Kolkata', '(GMT +5:30) India, Sri Lanka'), ('Asia/Kathmandu', '(GMT +5:45) Nepal'), ('Etc/GMT-6', '(GMT +6:00) Almaty, Dhaka, Colombo'), ('Indian/Cocos', '(GMT +6:30) Cocos Islands, Myanmar'), ('Etc/GMT-7', '(GMT +7:00) Bangkok, Hanoi, Jakarta'), ('Etc/GMT-8', '(GMT +8:00) Beijing, Perth, Singapore, Hong Kong'), ('Australia/Eucla', '(GMT +8:45) Australia (Eucla)'), ('Etc/GMT-9', '(GMT +9:00) Tokyo, Seoul, Osaka, Sapporo, Yakutsk'), ('Australia/North', '(GMT +9:30) Australia (Northern Territory)'), ('Etc/GMT-10', '(GMT +10:00) Eastern Australia, Guam, Vladivostok'), ('Etc/GMT-11', '(GMT +11:00) Magadan, Solomon Islands, New Caledonia'), ('Pacific/Norfolk', '(GMT +11:30) Norfolk Island'), ('Etc/GMT-12', '(GMT +12:00) Auckland, Wellington, Fiji, Kamchatka')], default='UTC', verbose_name='time zone', max_length=32), + preserve_default=True, + ), + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(max_length=30, help_text='Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters', verbose_name='username', validators=[django.core.validators.RegexValidator(re.compile('^[\\w.@+-]+$', 32), 'Enter a valid username.', 'invalid')], unique=True, db_index=True), + preserve_default=True, + ), + ] diff --git a/spirit/migrations/0004_topic_is_globally_pinned.py b/spirit/migrations/0004_topic_is_globally_pinned.py new file mode 100644 index 000000000..243b831b1 --- /dev/null +++ b/spirit/migrations/0004_topic_is_globally_pinned.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('spirit', '0003_auto_20141220_1125'), + ] + + operations = [ + migrations.AddField( + model_name='topic', + name='is_globally_pinned', + field=models.BooleanField(default=False, verbose_name='globally pinned'), + preserve_default=True, + ), + ] diff --git a/spirit/migrations/0005_auto_20150327_0138.py b/spirit/migrations/0005_auto_20150327_0138.py new file mode 100644 index 000000000..353277ea1 --- /dev/null +++ b/spirit/migrations/0005_auto_20150327_0138.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('spirit', '0004_topic_is_globally_pinned'), + ] + + operations = [ + migrations.AlterModelOptions( + name='category', + options={'ordering': ['title', 'pk'], 'verbose_name_plural': 'categories', 'verbose_name': 'category'}, + ), + migrations.AlterModelOptions( + name='comment', + options={'ordering': ['-date', '-pk'], 'verbose_name_plural': 'comments', 'verbose_name': 'comment'}, + ), + migrations.AlterModelOptions( + name='commentflag', + options={'ordering': ['-date', '-pk'], 'verbose_name_plural': 'comments flags', 'verbose_name': 'comment flag'}, + ), + migrations.AlterModelOptions( + name='commenthistory', + options={'ordering': ['-date', '-pk'], 'verbose_name_plural': 'comments history', 'verbose_name': 'comment history'}, + ), + migrations.AlterModelOptions( + name='commentlike', + options={'ordering': ['-date', '-pk'], 'verbose_name_plural': 'likes', 'verbose_name': 'like'}, + ), + migrations.AlterModelOptions( + name='flag', + options={'ordering': ['-date', '-pk'], 'verbose_name_plural': 'flags', 'verbose_name': 'flag'}, + ), + migrations.AlterModelOptions( + name='topic', + options={'ordering': ['-last_active', '-pk'], 'verbose_name_plural': 'topics', 'verbose_name': 'topic'}, + ), + migrations.AlterModelOptions( + name='topicfavorite', + options={'ordering': ['-date', '-pk'], 'verbose_name_plural': 'favorites', 'verbose_name': 'favorite'}, + ), + migrations.AlterModelOptions( + name='topicnotification', + options={'ordering': ['-date', '-pk'], 'verbose_name_plural': 'topics notification', 'verbose_name': 'topic notification'}, + ), + migrations.AlterModelOptions( + name='topicprivate', + options={'ordering': ['-date', '-pk'], 'verbose_name_plural': 'private topics', 'verbose_name': 'private topic'}, + ), + migrations.AlterModelOptions( + name='topicunread', + options={'ordering': ['-date', '-pk'], 'verbose_name_plural': 'topics unread', 'verbose_name': 'topic unread'}, + ), + migrations.AlterModelOptions( + name='user', + options={'ordering': ['-date_joined', '-pk'], 'verbose_name_plural': 'users', 'verbose_name': 'user'}, + ), + migrations.AddField( + model_name='user', + name='is_verified', + field=models.BooleanField(help_text='Designates whether the user has verified his account by email or by other means. Un-select this to let the user activate his account.', default=False, verbose_name='verified'), + preserve_default=True, + ), + ] diff --git a/spirit/migrations/0006_auto_20150327_0204.py b/spirit/migrations/0006_auto_20150327_0204.py new file mode 100644 index 000000000..da08de3db --- /dev/null +++ b/spirit/migrations/0006_auto_20150327_0204.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +def verify_active_users(apps, schema_editor): + User = apps.get_model(settings.AUTH_USER_MODEL) + User.objects.filter(is_active=True).update(is_verified=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('spirit', '0005_auto_20150327_0138'), + ] + + operations = [ + migrations.RunPython(verify_active_users), + ] diff --git a/spirit/models/category.py b/spirit/models/category.py index 64ed175b9..cf239f01b 100644 --- a/spirit/models/category.py +++ b/spirit/models/category.py @@ -8,7 +8,7 @@ from django.conf import settings from django.utils.encoding import python_2_unicode_compatible -from spirit.managers.category import CategoryManager +from spirit.managers.category import CategoryQuerySet from spirit.utils.models import AutoSlugField @@ -26,18 +26,24 @@ class Category(models.Model): # topic_count = models.PositiveIntegerField(_("topic count"), default=0) - objects = CategoryManager() + objects = CategoryQuerySet.as_manager() class Meta: - ordering = ['title', ] + ordering = ['title', 'pk'] verbose_name = _("category") verbose_name_plural = _("categories") + def __str__(self): + if self.parent: + return "%s, %s" % (self.parent.title, self.title) + else: + return self.title + def get_absolute_url(self): if self.pk == settings.ST_TOPIC_PRIVATE_CATEGORY_PK: return reverse('spirit:private-list') - - return reverse('spirit:category-detail', kwargs={'pk': str(self.id), 'slug': self.slug}) + else: + return reverse('spirit:category-detail', kwargs={'pk': str(self.id), 'slug': self.slug}) @property def is_subcategory(self): @@ -46,12 +52,6 @@ def is_subcategory(self): else: return False - def __str__(self): - if self.parent: - return "%s, %s" % (self.parent.title, self.title) - else: - return self.title - # def topic_posted_handler(sender, topic, **kwargs): # if topic.category.is_subcategory: diff --git a/spirit/models/comment.py b/spirit/models/comment.py index 8d8390855..c8bc239a9 100644 --- a/spirit/models/comment.py +++ b/spirit/models/comment.py @@ -6,14 +6,9 @@ from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from django.conf import settings -from django.db.models import F from django.utils.encoding import python_2_unicode_compatible -from ..signals.comment_like import comment_like_post_create, comment_like_post_delete -from ..signals.topic import topic_post_moderate - -from spirit.managers.comment import CommentManager -from ..signals.comment import comment_post_update +from ..managers.comment import CommentQuerySet COMMENT_MAX_LEN = 3000 # changing this needs migration @@ -47,37 +42,24 @@ class Comment(models.Model): modified_count = models.PositiveIntegerField(_("modified count"), default=0) likes_count = models.PositiveIntegerField(_("likes count"), default=0) - objects = CommentManager() + objects = CommentQuerySet.as_manager() class Meta: - ordering = ['-date', ] + ordering = ['-date', '-pk'] verbose_name = _("comment") verbose_name_plural = _("comments") - def get_absolute_url(self): - return reverse('spirit:comment-find', kwargs={'pk': str(self.id), }) - def __str__(self): return "%s: %s..." % (self.user.username, self.comment[:50]) + def get_absolute_url(self): + return reverse('spirit:comment-find', kwargs={'pk': str(self.id), }) -def comment_post_update_handler(sender, comment, **kwargs): - Comment.objects.filter(pk=comment.pk).update(modified_count=F('modified_count') + 1) - - -def comment_like_post_create_handler(sender, comment, **kwargs): - Comment.objects.filter(pk=comment.pk).update(likes_count=F('likes_count') + 1) - - -def comment_like_post_delete_handler(sender, comment, **kwargs): - Comment.objects.filter(pk=comment.pk).update(likes_count=F('likes_count') - 1) - - -def topic_post_moderate_handler(sender, user, topic, action, **kwargs): - Comment.objects.create(user=user, topic=topic, action=action, comment="action", comment_html="action") - - -comment_post_update.connect(comment_post_update_handler, dispatch_uid=__name__) -comment_like_post_create.connect(comment_like_post_create_handler, dispatch_uid=__name__) -comment_like_post_delete.connect(comment_like_post_delete_handler, dispatch_uid=__name__) -topic_post_moderate.connect(topic_post_moderate_handler, dispatch_uid=__name__) + @property + def like(self): + # *likes* is dinamically created by manager.with_likes() + try: + assert len(self.likes) <= 1, "Panic, too many likes" + return self.likes[0] + except (AttributeError, IndexError): + return diff --git a/spirit/models/comment_bookmark.py b/spirit/models/comment_bookmark.py index 89c20ce40..333f53aed 100644 --- a/spirit/models/comment_bookmark.py +++ b/spirit/models/comment_bookmark.py @@ -5,10 +5,10 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from django.db import IntegrityError from django.utils.encoding import python_2_unicode_compatible -from spirit.signals.topic import topic_viewed +from djconfig import config + from ..utils import paginator @@ -25,35 +25,12 @@ class Meta: verbose_name = _("comment bookmark") verbose_name_plural = _("comments bookmarks") + def __str__(self): + return "%s bookmarked comment %s in %s" \ + % (self.user.username, self.topic.title, self.comment_number) + def get_absolute_url(self): return paginator.get_url(self.topic.get_absolute_url(), self.comment_number, - settings.ST_COMMENTS_PER_PAGE, - settings.ST_COMMENTS_PAGE_VAR) - - def __str__(self): - return "%s bookmarked comment %s in %s" % (self.user.username, - self.topic.title, - self.comment_number) - - -def topic_page_viewed_handler(sender, request, topic, **kwargs): - if not request.user.is_authenticated(): - return - - try: - page_number = int(request.GET.get(settings.ST_COMMENTS_PAGE_VAR, 1)) - except ValueError: - return - - comment_number = settings.ST_COMMENTS_PER_PAGE * (page_number - 1) + 1 - - # TODO: use update_or_create on django 1.7 - try: - CommentBookmark.objects.create(user=request.user, topic=topic, comment_number=comment_number) - except IntegrityError: - CommentBookmark.objects.filter(user=request.user, topic=topic)\ - .update(comment_number=comment_number) - - -topic_viewed.connect(topic_page_viewed_handler, dispatch_uid=__name__) + config.comments_per_page, + 'page') diff --git a/spirit/models/comment_flag.py b/spirit/models/comment_flag.py index cdab98c16..4daf2bf31 100644 --- a/spirit/models/comment_flag.py +++ b/spirit/models/comment_flag.py @@ -24,16 +24,16 @@ class CommentFlag(models.Model): is_closed = models.BooleanField(default=False) class Meta: - ordering = ['-date', ] + ordering = ['-date', '-pk'] verbose_name = _("comment flag") verbose_name_plural = _("comments flags") - # def get_absolute_url(self): - # pass - def __str__(self): return "%s flagged" % self.comment + # def get_absolute_url(self): + # pass + @python_2_unicode_compatible class Flag(models.Model): @@ -47,7 +47,7 @@ class Flag(models.Model): class Meta: unique_together = ('user', 'comment') - ordering = ['-date', ] + ordering = ['-date', '-pk'] verbose_name = _("flag") verbose_name_plural = _("flags") diff --git a/spirit/models/comment_history.py b/spirit/models/comment_history.py index a3e46b49d..08cbcb674 100644 --- a/spirit/models/comment_history.py +++ b/spirit/models/comment_history.py @@ -7,8 +7,6 @@ from django.core.urlresolvers import reverse from django.utils.encoding import python_2_unicode_compatible -from spirit.signals.comment import comment_pre_update, comment_post_update - @python_2_unicode_compatible class CommentHistory(models.Model): @@ -19,28 +17,12 @@ class CommentHistory(models.Model): date = models.DateTimeField(auto_now_add=True) class Meta: - ordering = ['-date', ] + ordering = ['-date', '-pk'] verbose_name = _("comment history") verbose_name_plural = _("comments history") - def get_absolute_url(self): - return reverse('spirit:comment-history', kwargs={'pk': str(self.id), }) - def __str__(self): return "%s: %s..." % (self.comment_fk.user.username, self.comment_html[:50]) - -def comment_pre_update_handler(sender, comment, **kwargs): - # save original comment - exists = CommentHistory.objects.filter(comment_fk=comment).exists() - - if not exists: - CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html, date=comment.date) - - -def comment_post_update_handler(sender, comment, **kwargs): - CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html) - - -comment_pre_update.connect(comment_pre_update_handler, dispatch_uid=__name__) -comment_post_update.connect(comment_post_update_handler, dispatch_uid=__name__) + def get_absolute_url(self): + return reverse('spirit:comment-history', kwargs={'pk': str(self.id), }) diff --git a/spirit/models/comment_like.py b/spirit/models/comment_like.py index 39264f1f0..709b37087 100644 --- a/spirit/models/comment_like.py +++ b/spirit/models/comment_like.py @@ -8,8 +8,6 @@ from django.core.urlresolvers import reverse from django.utils.encoding import python_2_unicode_compatible -from ..managers.comment_like import CommentLikeManager - @python_2_unicode_compatible class CommentLike(models.Model): @@ -19,16 +17,14 @@ class CommentLike(models.Model): date = models.DateTimeField(auto_now_add=True) - objects = CommentLikeManager() - class Meta: unique_together = ('user', 'comment') - ordering = ['-date', ] + ordering = ['-date', '-pk'] verbose_name = _("like") verbose_name_plural = _("likes") - def get_delete_url(self): - return reverse('spirit:like-delete', kwargs={'pk': str(self.pk), }) - def __str__(self): return "%s likes %s" % (self.user, self.comment) + + def get_delete_url(self): + return reverse('spirit:like-delete', kwargs={'pk': str(self.pk), }) diff --git a/spirit/models/topic.py b/spirit/models/topic.py index 427697ba8..1255db6ad 100644 --- a/spirit/models/topic.py +++ b/spirit/models/topic.py @@ -6,14 +6,9 @@ from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from django.conf import settings -from django.db.models import F -from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible -from ..signals.comment import comment_posted, comment_moved - -from spirit.signals.topic import topic_viewed -from spirit.managers.topic import TopicManager +from spirit.managers.topic import TopicQuerySet from spirit.utils.models import AutoSlugField @@ -23,53 +18,44 @@ class Topic(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("user")) category = models.ForeignKey('spirit.Category', verbose_name=_("category")) - title = models.CharField(_("title"), max_length=75) + title = models.CharField(_("title"), max_length=255) slug = AutoSlugField(populate_from="title", db_index=False, blank=True) date = models.DateTimeField(_("date"), auto_now_add=True) last_active = models.DateTimeField(_("last active"), auto_now_add=True) + is_pinned = models.BooleanField(_("pinned"), default=False) + is_globally_pinned = models.BooleanField(_("globally pinned"), default=False) is_closed = models.BooleanField(_("closed"), default=False) is_removed = models.BooleanField(default=False) view_count = models.PositiveIntegerField(_("views count"), default=0) comment_count = models.PositiveIntegerField(_("comment count"), default=0) - objects = TopicManager() + objects = TopicQuerySet.as_manager() class Meta: - ordering = ['-last_active', ] + ordering = ['-last_active', '-pk'] verbose_name = _("topic") verbose_name_plural = _("topics") + def __str__(self): + return self.title + def get_absolute_url(self): if self.category_id == settings.ST_TOPIC_PRIVATE_CATEGORY_PK: return reverse('spirit:private-detail', kwargs={'topic_id': str(self.id), 'slug': self.slug}) - - return reverse('spirit:topic-detail', kwargs={'pk': str(self.id), 'slug': self.slug}) + else: + return reverse('spirit:topic-detail', kwargs={'pk': str(self.id), 'slug': self.slug}) @property def main_category(self): return self.category.parent or self.category - def __str__(self): - return self.title - - -def topic_page_viewed_handler(sender, request, topic, **kwargs): - Topic.objects.filter(pk=topic.pk)\ - .update(view_count=F('view_count') + 1) - - -def comment_posted_handler(sender, comment, **kwargs): - Topic.objects.filter(pk=comment.topic.pk)\ - .update(comment_count=F('comment_count') + 1, last_active=timezone.now()) - - -def comment_moved_handler(sender, comments, topic_from, **kwargs): - Topic.objects.filter(pk=topic_from.pk)\ - .update(comment_count=F('comment_count') - len(comments)) - - -topic_viewed.connect(topic_page_viewed_handler, dispatch_uid=__name__) -comment_posted.connect(comment_posted_handler, dispatch_uid=__name__) -comment_moved.connect(comment_moved_handler, dispatch_uid=__name__) + @property + def bookmark(self): + # *bookmarks* is dinamically created by manager.with_bookmarks() + try: + assert len(self.bookmarks) <= 1, "Panic, too many bookmarks" + return self.bookmarks[0] + except (AttributeError, IndexError): + return diff --git a/spirit/models/topic_favorite.py b/spirit/models/topic_favorite.py index 166ac63d6..202457ad3 100644 --- a/spirit/models/topic_favorite.py +++ b/spirit/models/topic_favorite.py @@ -18,7 +18,7 @@ class TopicFavorite(models.Model): class Meta: unique_together = ('user', 'topic') - ordering = ['-date', ] + ordering = ['-date', '-pk'] verbose_name = _("favorite") verbose_name_plural = _("favorites") diff --git a/spirit/models/topic_notification.py b/spirit/models/topic_notification.py index f9b0c8a9a..0e7de5abe 100644 --- a/spirit/models/topic_notification.py +++ b/spirit/models/topic_notification.py @@ -5,15 +5,9 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from django.utils import timezone -from django.db import IntegrityError from django.utils.encoding import python_2_unicode_compatible -from spirit.signals.comment import comment_posted -from spirit.signals.topic_private import topic_private_post_create, topic_private_access_pre_create -from spirit.signals.topic import topic_viewed - -from spirit.managers.topic_notifications import TopicNotificationManager +from spirit.managers.topic_notifications import TopicNotificationQuerySet UNDEFINED, MENTION, COMMENT = range(3) @@ -37,14 +31,17 @@ class TopicNotification(models.Model): is_read = models.BooleanField(default=False) is_active = models.BooleanField(default=False) - objects = TopicNotificationManager() + objects = TopicNotificationQuerySet.as_manager() class Meta: unique_together = ('user', 'topic') - ordering = ['-date', ] + ordering = ['-date', '-pk'] verbose_name = _("topic notification") verbose_name_plural = _("topics notification") + def __str__(self): + return "%s in %s" % (self.user, self.topic) + def get_absolute_url(self): return self.comment.get_absolute_url() @@ -59,76 +56,3 @@ def is_mention(self): @property def is_comment(self): return self.action == COMMENT - - def __str__(self): - return "%s in %s" % (self.user, self.topic) - - -def notification_comment_posted_handler(sender, comment, **kwargs): - # Create Notification for poster - # if not exists create a dummy one with defaults - try: - TopicNotification.objects.get_or_create(user=comment.user, topic=comment.topic, - defaults={'action': COMMENT, - 'is_read': True, - 'is_active': True}) - except IntegrityError: - pass - - TopicNotification.objects.filter(topic=comment.topic, is_active=True, is_read=True)\ - .exclude(user=comment.user)\ - .update(comment=comment, is_read=False, action=COMMENT, date=timezone.now()) - - -def mention_comment_posted_handler(sender, comment, mentions, **kwargs): - if not mentions: - return - - for username, user in mentions.items(): - try: - TopicNotification.objects.create(user=user, topic=comment.topic, - comment=comment, action=MENTION) - except IntegrityError: - pass - - TopicNotification.objects.filter(user__in=mentions.values(), topic=comment.topic, is_read=True)\ - .update(comment=comment, is_read=False, action=MENTION, date=timezone.now()) - - -def comment_posted_handler(sender, comment, mentions, **kwargs): - notification_comment_posted_handler(sender, comment, **kwargs) - mention_comment_posted_handler(sender, comment, mentions, **kwargs) - - -def topic_private_post_create_handler(sender, topics_private, comment, **kwargs): - # topic.user notification is created on comment_posted - TopicNotification.objects.bulk_create([TopicNotification(user=tp.user, topic=tp.topic, - comment=comment, action=COMMENT, - is_active=True) - for tp in topics_private - if tp.user != tp.topic.user]) - - -def topic_private_access_pre_create_handler(sender, topic, user, **kwargs): - # TODO: use update_or_create on django 1.7 - # change to post create - try: - TopicNotification.objects.create(user=user, topic=topic, - comment=topic.comment_set.last(), action=COMMENT, - is_active=True) - except IntegrityError: - pass - - -def topic_viewed_handler(sender, request, topic, **kwargs): - if not request.user.is_authenticated(): - return - - TopicNotification.objects.filter(user=request.user, topic=topic)\ - .update(is_read=True) - - -comment_posted.connect(comment_posted_handler, dispatch_uid=__name__) -topic_private_post_create.connect(topic_private_post_create_handler, dispatch_uid=__name__) -topic_private_access_pre_create.connect(topic_private_access_pre_create_handler, dispatch_uid=__name__) -topic_viewed.connect(topic_viewed_handler, dispatch_uid=__name__) diff --git a/spirit/models/topic_poll.py b/spirit/models/topic_poll.py index 205d04886..19608629b 100644 --- a/spirit/models/topic_poll.py +++ b/spirit/models/topic_poll.py @@ -6,9 +6,6 @@ from django.utils.translation import ugettext_lazy as _ from django.conf import settings from django.utils.encoding import python_2_unicode_compatible -from django.db.models import F - -from spirit.signals.topic_poll import topic_poll_post_vote, topic_poll_pre_vote @python_2_unicode_compatible @@ -24,11 +21,15 @@ class Meta: verbose_name = _("topic poll") verbose_name_plural = _("topics polls") + def __str__(self): + return "poll at topic #%s" % self.topic.pk + def get_absolute_url(self): return self.topic.get_absolute_url() - def __str__(self): - return "poll at topic #%s" % self.topic.pk + @property + def is_multiple_choice(self): + return self.choice_limit > 1 @python_2_unicode_compatible @@ -62,17 +63,3 @@ class Meta: def __str__(self): return "poll vote %s" % self.pk - - -def poll_pre_vote(sender, poll, user, **kwargs): - TopicPollChoice.objects.filter(poll=poll, votes__user=user)\ - .update(vote_count=F('vote_count') - 1) - - -def poll_post_vote(sender, poll, user, **kwargs): - TopicPollChoice.objects.filter(poll=poll, votes__user=user)\ - .update(vote_count=F('vote_count') + 1) - - -topic_poll_pre_vote.connect(poll_pre_vote, dispatch_uid=__name__) -topic_poll_post_vote.connect(poll_post_vote, dispatch_uid=__name__) diff --git a/spirit/models/topic_private.py b/spirit/models/topic_private.py index cece2014a..e651ac487 100644 --- a/spirit/models/topic_private.py +++ b/spirit/models/topic_private.py @@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import python_2_unicode_compatible -from spirit.managers.topic_private import TopicPrivateManager +from spirit.managers.topic_private import TopicPrivateQuerySet @python_2_unicode_compatible @@ -18,16 +18,16 @@ class TopicPrivate(models.Model): date = models.DateTimeField(auto_now_add=True) - objects = TopicPrivateManager() + objects = TopicPrivateQuerySet.as_manager() class Meta: unique_together = ('user', 'topic') - ordering = ['-date', ] + ordering = ['-date', '-pk'] verbose_name = _("private topic") verbose_name_plural = _("private topics") - def get_absolute_url(self): - return self.topic.get_absolute_url() - def __str__(self): return "%s participes in %s" % (self.user, self.topic) + + def get_absolute_url(self): + return self.topic.get_absolute_url() diff --git a/spirit/models/topic_unread.py b/spirit/models/topic_unread.py index a36cde5d7..a27a775f0 100644 --- a/spirit/models/topic_unread.py +++ b/spirit/models/topic_unread.py @@ -5,15 +5,8 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from django.utils import timezone -from django.db import IntegrityError from django.utils.encoding import python_2_unicode_compatible -from spirit.signals.comment import comment_posted - -from spirit.managers.topic_unread import TopicUnreadManager -from spirit.signals.topic import topic_viewed - @python_2_unicode_compatible class TopicUnread(models.Model): @@ -24,38 +17,14 @@ class TopicUnread(models.Model): date = models.DateTimeField(auto_now_add=True) is_read = models.BooleanField(default=True) - objects = TopicUnreadManager() - class Meta: unique_together = ('user', 'topic') - ordering = ['-date', ] + ordering = ['-date', '-pk'] verbose_name = _("topic unread") verbose_name_plural = _("topics unread") - def get_absolute_url(self): - return self.topic.get_absolute_url() - def __str__(self): return "%s read %s" % (self.user, self.topic) - -def topic_page_viewed_handler(sender, request, topic, **kwargs): - if not request.user.is_authenticated(): - return - - # TODO: use update_or_create on django 1.7 - try: - TopicUnread.objects.create(user=request.user, topic=topic) - except IntegrityError: - TopicUnread.objects.filter(user=request.user, topic=topic)\ - .update(is_read=True) - - -def comment_posted_handler(sender, comment, **kwargs): - TopicUnread.objects.filter(topic=comment.topic)\ - .exclude(user=comment.user)\ - .update(is_read=False, date=timezone.now()) - - -topic_viewed.connect(topic_page_viewed_handler, dispatch_uid=__name__) -comment_posted.connect(comment_posted_handler, dispatch_uid=__name__) + def get_absolute_url(self): + return self.topic.get_absolute_url() diff --git a/spirit/models/user.py b/spirit/models/user.py index 96fdb6f1c..dca95956d 100644 --- a/spirit/models/user.py +++ b/spirit/models/user.py @@ -17,7 +17,6 @@ class AbstractForumUser(models.Model): - slug = AutoSlugField(populate_from="username", db_index=False, blank=True) location = models.CharField(_("location"), max_length=75, blank=True) last_seen = models.DateTimeField(_("last seen"), auto_now=True) @@ -25,7 +24,10 @@ class AbstractForumUser(models.Model): timezone = models.CharField(_("time zone"), max_length=32, choices=TIMEZONE_CHOICES, default='UTC') is_administrator = models.BooleanField(_('administrator status'), default=False) is_moderator = models.BooleanField(_('moderator status'), default=False) - # is_verified = models.BooleanField(_('verified'), default=False) + is_verified = models.BooleanField(_('verified'), default=False, + help_text=_('Designates whether the user has verified his ' + 'account by email or by other means. Un-select this ' + 'to let the user activate his account.')) topic_count = models.PositiveIntegerField(_("topic count"), default=0) comment_count = models.PositiveIntegerField(_("comment count"), default=0) @@ -92,6 +94,6 @@ class User(AbstractUser): class Meta(AbstractUser.Meta): swappable = 'AUTH_USER_MODEL' app_label = 'spirit' - ordering = ['-date_joined', ] + ordering = ['-date_joined', '-pk'] verbose_name = _('user') verbose_name_plural = _('users') diff --git a/spirit/settings.py b/spirit/settings.py index 223c7545a..4ae5be17e 100644 --- a/spirit/settings.py +++ b/spirit/settings.py @@ -6,9 +6,6 @@ from __future__ import unicode_literals import os -ST_COMMENTS_PER_PAGE = 20 -ST_COMMENTS_PAGE_VAR = 'page' - ST_TOPIC_PRIVATE_CATEGORY_PK = 1 ST_UNCATEGORIZED_CATEGORY_PK = 2 @@ -30,6 +27,8 @@ ST_ALLOWED_UPLOAD_IMAGE_FORMAT = ('jpeg', 'png', 'gif') +ST_INITIAL_MIGRATION_DEPENDENCIES = [] # [('myuser', '0001_initial'), ] + # # Django & Spirit settings defined below... # @@ -91,13 +90,6 @@ 'django.contrib.messages.context_processors.messages', ) -# Keep templates in memory -TEMPLATE_LOADERS = ( - ('django.template.loaders.cached.Loader', ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - )), -) # # Third-party apps settings defined below... @@ -114,6 +106,7 @@ CACHES.update({ 'djconfig': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'unique-config', }, }) diff --git a/spirit/signals/handlers/__init__.py b/spirit/signals/handlers/__init__.py new file mode 100644 index 000000000..bcae9a39f --- /dev/null +++ b/spirit/signals/handlers/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from . import comment +from . import comment_bookmark +from . import comment_history +from . import topic +from . import topic_notification +from . import topic_poll +from . import topic_unread diff --git a/spirit/signals/handlers/comment.py b/spirit/signals/handlers/comment.py new file mode 100644 index 000000000..89d68793d --- /dev/null +++ b/spirit/signals/handlers/comment.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.db.models import F + +from spirit.models import Comment +from ..comment import comment_post_update +from ..comment_like import comment_like_post_create, comment_like_post_delete +from ..topic_moderate import topic_post_moderate + + +def comment_post_update_handler(sender, comment, **kwargs): + Comment.objects.filter(pk=comment.pk).update(modified_count=F('modified_count') + 1) + + +def comment_like_post_create_handler(sender, comment, **kwargs): + Comment.objects.filter(pk=comment.pk).update(likes_count=F('likes_count') + 1) + + +def comment_like_post_delete_handler(sender, comment, **kwargs): + Comment.objects.filter(pk=comment.pk).update(likes_count=F('likes_count') - 1) + + +def topic_post_moderate_handler(sender, user, topic, action, **kwargs): + Comment.objects.create(user=user, topic=topic, action=action, comment="action", comment_html="action") + + +comment_post_update.connect(comment_post_update_handler, dispatch_uid=__name__) +comment_like_post_create.connect(comment_like_post_create_handler, dispatch_uid=__name__) +comment_like_post_delete.connect(comment_like_post_delete_handler, dispatch_uid=__name__) +topic_post_moderate.connect(topic_post_moderate_handler, dispatch_uid=__name__) diff --git a/spirit/signals/handlers/comment_bookmark.py b/spirit/signals/handlers/comment_bookmark.py new file mode 100644 index 000000000..2678973fd --- /dev/null +++ b/spirit/signals/handlers/comment_bookmark.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from djconfig import config + +from ...models.comment_bookmark import CommentBookmark +from ..topic import topic_viewed + + +def topic_page_viewed_handler(sender, request, topic, **kwargs): + if not request.user.is_authenticated(): + return + + try: + page_number = int(request.GET.get('page', 1)) + except ValueError: + return + + comment_number = config.comments_per_page * (page_number - 1) + 1 + + CommentBookmark.objects.update_or_create(user=request.user, topic=topic, + defaults={'comment_number': comment_number, }) + + +topic_viewed.connect(topic_page_viewed_handler, dispatch_uid=__name__) diff --git a/spirit/signals/handlers/comment_history.py b/spirit/signals/handlers/comment_history.py new file mode 100644 index 000000000..43bd0c7ac --- /dev/null +++ b/spirit/signals/handlers/comment_history.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from spirit.models import CommentHistory +from ..comment import comment_pre_update, comment_post_update + + +def comment_pre_update_handler(sender, comment, **kwargs): + # Save original comment + exists = CommentHistory.objects.filter(comment_fk=comment).exists() + + if not exists: + CommentHistory.objects.create(comment_fk=comment, + comment_html=comment.comment_html, + date=comment.date) + + +def comment_post_update_handler(sender, comment, **kwargs): + CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html) + + +comment_pre_update.connect(comment_pre_update_handler, dispatch_uid=__name__) +comment_post_update.connect(comment_post_update_handler, dispatch_uid=__name__) diff --git a/spirit/signals/handlers/topic.py b/spirit/signals/handlers/topic.py new file mode 100644 index 000000000..e40c32382 --- /dev/null +++ b/spirit/signals/handlers/topic.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.db.models import F +from django.utils import timezone + +from spirit.models import Topic +from ..comment import comment_posted, comment_moved +from ..topic import topic_viewed + + +def topic_page_viewed_handler(sender, request, topic, **kwargs): + Topic.objects.filter(pk=topic.pk)\ + .update(view_count=F('view_count') + 1) + + +def comment_posted_handler(sender, comment, **kwargs): + Topic.objects.filter(pk=comment.topic.pk)\ + .update(comment_count=F('comment_count') + 1, last_active=timezone.now()) + + +def comment_moved_handler(sender, comments, topic_from, **kwargs): + Topic.objects.filter(pk=topic_from.pk)\ + .update(comment_count=F('comment_count') - len(comments)) + + +topic_viewed.connect(topic_page_viewed_handler, dispatch_uid=__name__) +comment_posted.connect(comment_posted_handler, dispatch_uid=__name__) +comment_moved.connect(comment_moved_handler, dispatch_uid=__name__) diff --git a/spirit/signals/handlers/topic_notification.py b/spirit/signals/handlers/topic_notification.py new file mode 100644 index 000000000..9833bc308 --- /dev/null +++ b/spirit/signals/handlers/topic_notification.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.utils import timezone +from django.db import IntegrityError, transaction + +from spirit.models.topic_notification import TopicNotification, COMMENT, MENTION +from ..comment import comment_posted +from ..topic_private import topic_private_post_create, topic_private_access_pre_create +from ..topic import topic_viewed + + +def notification_comment_posted_handler(sender, comment, **kwargs): + # Create Notification for poster + # if not exists create a dummy one with defaults + try: + TopicNotification.objects.get_or_create(user=comment.user, topic=comment.topic, + defaults={'action': COMMENT, + 'is_read': True, + 'is_active': True}) + except IntegrityError: + pass + + TopicNotification.objects.filter(topic=comment.topic, is_active=True, is_read=True)\ + .exclude(user=comment.user)\ + .update(comment=comment, is_read=False, action=COMMENT, date=timezone.now()) + + +def mention_comment_posted_handler(sender, comment, mentions, **kwargs): + if not mentions: + return + + for username, user in mentions.items(): + try: + with transaction.atomic(): + TopicNotification.objects.create(user=user, topic=comment.topic, + comment=comment, action=MENTION) + except IntegrityError: + pass + + TopicNotification.objects.filter(user__in=mentions.values(), topic=comment.topic, is_read=True)\ + .update(comment=comment, is_read=False, action=MENTION, date=timezone.now()) + + +def comment_posted_handler(sender, comment, mentions, **kwargs): + notification_comment_posted_handler(sender, comment, **kwargs) + mention_comment_posted_handler(sender, comment, mentions, **kwargs) + + +def topic_private_post_create_handler(sender, topics_private, comment, **kwargs): + # topic.user notification is created on comment_posted + TopicNotification.objects.bulk_create([TopicNotification(user=tp.user, topic=tp.topic, + comment=comment, action=COMMENT, + is_active=True) + for tp in topics_private + if tp.user != tp.topic.user]) + + +def topic_private_access_pre_create_handler(sender, topic, user, **kwargs): + # TODO: use update_or_create on django 1.7 + # change to post create + try: + with transaction.atomic(): + TopicNotification.objects.create(user=user, topic=topic, + comment=topic.comment_set.last(), action=COMMENT, + is_active=True) + except IntegrityError: + pass + + +def topic_viewed_handler(sender, request, topic, **kwargs): + if not request.user.is_authenticated(): + return + + TopicNotification.objects.filter(user=request.user, topic=topic)\ + .update(is_read=True) + + +comment_posted.connect(comment_posted_handler, dispatch_uid=__name__) +topic_private_post_create.connect(topic_private_post_create_handler, dispatch_uid=__name__) +topic_private_access_pre_create.connect(topic_private_access_pre_create_handler, dispatch_uid=__name__) +topic_viewed.connect(topic_viewed_handler, dispatch_uid=__name__) diff --git a/spirit/signals/handlers/topic_poll.py b/spirit/signals/handlers/topic_poll.py new file mode 100644 index 000000000..108e18aff --- /dev/null +++ b/spirit/signals/handlers/topic_poll.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.db.models import F + +from spirit.models import TopicPollChoice +from ..topic_poll import topic_poll_post_vote, topic_poll_pre_vote + + +def poll_pre_vote(sender, poll, user, **kwargs): + TopicPollChoice.objects.filter(poll=poll, votes__user=user)\ + .update(vote_count=F('vote_count') - 1) + + +def poll_post_vote(sender, poll, user, **kwargs): + TopicPollChoice.objects.filter(poll=poll, votes__user=user)\ + .update(vote_count=F('vote_count') + 1) + + +topic_poll_pre_vote.connect(poll_pre_vote, dispatch_uid=__name__) +topic_poll_post_vote.connect(poll_post_vote, dispatch_uid=__name__) diff --git a/spirit/signals/handlers/topic_unread.py b/spirit/signals/handlers/topic_unread.py new file mode 100644 index 000000000..6e9f35137 --- /dev/null +++ b/spirit/signals/handlers/topic_unread.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.utils import timezone +from django.db import IntegrityError + +from spirit.models import TopicUnread +from ..comment import comment_posted +from ..topic import topic_viewed + + +def topic_page_viewed_handler(sender, request, topic, **kwargs): + if not request.user.is_authenticated(): + return + + try: + TopicUnread.objects.update_or_create(user=request.user, topic=topic, + defaults={'is_read': True, }) + except IntegrityError: + pass + + +def comment_posted_handler(sender, comment, **kwargs): + TopicUnread.objects.filter(topic=comment.topic)\ + .exclude(user=comment.user)\ + .update(is_read=False, date=timezone.now()) + + +topic_viewed.connect(topic_page_viewed_handler, dispatch_uid=__name__) +comment_posted.connect(comment_posted_handler, dispatch_uid=__name__) diff --git a/spirit/signals/topic.py b/spirit/signals/topic.py index 00bb2bd7a..c835ee0fa 100644 --- a/spirit/signals/topic.py +++ b/spirit/signals/topic.py @@ -6,4 +6,3 @@ topic_viewed = Signal(providing_args=['request', 'topic']) -topic_post_moderate = Signal(providing_args=['user', 'topic', 'action']) diff --git a/spirit/signals/topic_moderate.py b/spirit/signals/topic_moderate.py new file mode 100644 index 000000000..3c4dfe104 --- /dev/null +++ b/spirit/signals/topic_moderate.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.dispatch import Signal + + +topic_post_moderate = Signal(providing_args=['user', 'topic', 'action']) diff --git a/spirit/static/spirit/stylesheets/elements/_forms.scss b/spirit/static/spirit/stylesheets/elements/_forms.scss index d66c68265..48a7164ed 100644 --- a/spirit/static/spirit/stylesheets/elements/_forms.scss +++ b/spirit/static/spirit/stylesheets/elements/_forms.scss @@ -13,6 +13,8 @@ textarea { input[type="text"], input[type="password"], input[type="email"], +input[type="search"], +input[type="number"], select, textarea { width: 100%; @@ -23,6 +25,8 @@ textarea { input[type="text"]:focus, input[type="password"]:focus, input[type="email"]:focus, +input[type="search"]:focus, +input[type="number"]:focus, select:focus, textarea:focus { border: 1px solid $red; @@ -44,4 +48,4 @@ label { margin: $half-spacing-size / 2 $half-spacing-size; margin-bottom: 0; } -} \ No newline at end of file +} diff --git a/spirit/static/spirit/stylesheets/styles.css b/spirit/static/spirit/stylesheets/styles.css index a1268ca90..3752436f8 100644 --- a/spirit/static/spirit/stylesheets/styles.css +++ b/spirit/static/spirit/stylesheets/styles.css @@ -471,6 +471,8 @@ textarea { input[type="text"], input[type="password"], input[type="email"], +input[type="search"], +input[type="number"], select, textarea { width: 100%; @@ -480,6 +482,8 @@ textarea { input[type="text"]:focus, input[type="password"]:focus, input[type="email"]:focus, +input[type="search"]:focus, +input[type="number"]:focus, select:focus, textarea:focus { border: 1px solid #c65555; } diff --git a/spirit/templates/spirit/_base.html b/spirit/templates/spirit/_base.html index 27397469e..e54be366c 100644 --- a/spirit/templates/spirit/_base.html +++ b/spirit/templates/spirit/_base.html @@ -1,4 +1,5 @@ {% load spirit_tags i18n %} +{% load static from staticfiles %}
@@ -9,34 +10,34 @@ - - - - + + + + - - - - - - + + + + + + {% if user.is_authenticated %} - - - - - - - - - - - + + + + + + + + + + + {% endif %} {% if user.is_moderator %} - + {% endif %} \ No newline at end of file + diff --git a/spirit/templates/spirit/comment/_render_list.html b/spirit/templates/spirit/comment/_render_list.html index 1c5fd65d8..b73b99e72 100644 --- a/spirit/templates/spirit/comment/_render_list.html +++ b/spirit/templates/spirit/comment/_render_list.html @@ -1,14 +1,10 @@ {% load spirit_tags i18n %} -{% if user.is_authenticated %} - {% populate_likes comments=page user=user %} -{% endif %} -
{% trans "Comment history" %}
{% trans "Comment history" %}
{% trans "Search" %}
{% else %}{% trans "Results" %}
- {% yt_paginator_autopaginate page as page %} {% get_topics_from_search_result results=page as topics_page %} {% if topics_page %} - {% include "spirit/topic/_render_list.html" with page=topics_page %} - {% render_yt_paginator page %} + {% include "spirit/topic/_render_list.html" with topics=topics_page %} + {% render_paginator page %} {% else %}{% trans "There are no search results." %}
{% endif %} {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/spirit/templates/spirit/topic/_render_list.html b/spirit/templates/spirit/topic/_render_list.html index 779577b05..46663b565 100644 --- a/spirit/templates/spirit/topic/_render_list.html +++ b/spirit/templates/spirit/topic/_render_list.html @@ -1,17 +1,13 @@ {% load spirit_tags i18n %} -{% if user.is_authenticated %} - {% populate_bookmarks topics=page user=user %} -{% endif %} - {# topic list #}{% trans "There are no topics here, yet" %}
{% endfor %} -- {% if topic.is_pinned %} + {% if topic.is_pinned or topic.is_globally_pinned %} {% endif %} {% if topic.is_closed %} @@ -58,6 +58,12 @@
{% else %}
{% render_poll_form topic=topic user=user %} - {% get_comment_list topic=topic as comments %} - {% paginator_autopaginate comments per_page=COMMENTS_PER_PAGE as page %} - {% include "spirit/comment/_render_list.html" %}
- {% render_paginator page %}
+ {% render_paginator page=comments %}
{% if user.is_authenticated %}
@@ -128,4 +131,4 @@
{% include "spirit/topic_notification/_render_list.html" %}
- {% render_yt_paginator page %}
+ {% render_paginator notifications %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/spirit/templates/spirit/topic_notification/list_unread.html b/spirit/templates/spirit/topic_notification/list_unread.html
index 169cae4d4..aa868eeaa 100644
--- a/spirit/templates/spirit/topic_notification/list_unread.html
+++ b/spirit/templates/spirit/topic_notification/list_unread.html
@@ -10,7 +10,7 @@
{% if page %}
- {% include "spirit/topic_notification/_render_list.html" %}
+ {% include "spirit/topic_notification/_render_list.html" with notifications=page %}
{% else %}
- {% for t in page %}
+ {% for t in topics %}
{{ t.title }} {% trans "join" %}
@@ -22,6 +20,6 @@
- {% render_yt_paginator page %}
+ {% render_paginator topics %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/spirit/templates/spirit/topic_private/private_detail.html b/spirit/templates/spirit/topic_private/private_detail.html
index 1a5507a8f..bceef63cc 100644
--- a/spirit/templates/spirit/topic_private/private_detail.html
+++ b/spirit/templates/spirit/topic_private/private_detail.html
@@ -6,9 +6,6 @@
{% block content %}
- {% get_comment_list topic=topic as comments %}
- {% paginator_autopaginate comments per_page=COMMENTS_PER_PAGE as page %}
-
- {% trans "Private topics" %}
@@ -41,7 +38,7 @@
- {% render_paginator page %}
+ {% render_paginator comments %}
{% render_notification_form user=user topic=topic %}
@@ -74,4 +71,4 @@
- {% for c in page %}
+ {% for c in comments %}
@@ -47,4 +47,4 @@
});
-
\ No newline at end of file
+
diff --git a/spirit/templates/spirit/user/profile_comments.html b/spirit/templates/spirit/user/profile_comments.html
index f5d842082..dde3b9284 100644
--- a/spirit/templates/spirit/user/profile_comments.html
+++ b/spirit/templates/spirit/user/profile_comments.html
@@ -8,10 +8,8 @@
{% include "spirit/user/_profile.html" with active_tab=0 %}
- {% yt_paginator_autopaginate comments as page %}
-
{% include "spirit/user/_render_comments_list.html" %}
- {% render_yt_paginator page %}
+ {% render_paginator comments %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/spirit/templates/spirit/user/profile_likes.html b/spirit/templates/spirit/user/profile_likes.html
index 3b6d671f0..8ca3f3f8a 100644
--- a/spirit/templates/spirit/user/profile_likes.html
+++ b/spirit/templates/spirit/user/profile_likes.html
@@ -8,10 +8,8 @@
{% include "spirit/user/_profile.html" with active_tab=2 %}
- {% yt_paginator_autopaginate comments as page %}
-
{% include "spirit/user/_render_comments_list.html" %}
- {% render_yt_paginator page %}
+ {% render_paginator comments %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/spirit/templates/spirit/user/profile_topics.html b/spirit/templates/spirit/user/profile_topics.html
index f20a14fc5..778e16aa8 100644
--- a/spirit/templates/spirit/user/profile_topics.html
+++ b/spirit/templates/spirit/user/profile_topics.html
@@ -1,6 +1,6 @@
{% extends "spirit/_base.html" %}
-{% load i18n %}
+{% load spirit_tags i18n %}
{% block title %}{{ p_user.username }} {% trans "topics" %}{% endblock %}
@@ -8,7 +8,8 @@
{% include "spirit/user/_profile.html" with active_tab=1 %}
- {# Topics #}
- {% include "spirit/topic/_render_page_list.html" %}
+ {% include "spirit/topic/_render_list.html" %}
-{% endblock %}
\ No newline at end of file
+ {% render_paginator topics %}
+
+{% endblock %}
diff --git a/spirit/templatetags/spirit_tags.py b/spirit/templatetags/spirit_tags.py
index e3966d2ce..89d2fcec5 100644
--- a/spirit/templatetags/spirit_tags.py
+++ b/spirit/templatetags/spirit_tags.py
@@ -1,14 +1,13 @@
# -*- coding: utf-8 -*-
from .tags import comment
-from .tags import comment_bookmark
from .tags import comment_like
from .tags import topic_poll
from .tags import search
from .tags import topic_favorite
from .tags import topic_notification
from .tags import topic_private
-from .tags.utils import filters
+from .tags.utils import avatar
from .tags.utils import gravatar
from .tags.utils import messages
from .tags.utils import paginator
@@ -17,7 +16,7 @@
from .tags import register
__all__ = [
- 'comment', 'comment_bookmark', 'comment_like', 'topic_poll', 'search',
- 'topic_favorite', 'topic_notification', 'topic_private', 'filters',
+ 'comment', 'comment_like', 'topic_poll', 'search',
+ 'topic_favorite', 'topic_notification', 'topic_private', 'avatar',
'gravatar', 'messages', 'paginator', 'social_share', 'time', 'register'
]
diff --git a/spirit/templatetags/tags/comment.py b/spirit/templatetags/tags/comment.py
index a5104b63c..1e0e54a88 100644
--- a/spirit/templatetags/tags/comment.py
+++ b/spirit/templatetags/tags/comment.py
@@ -2,23 +2,17 @@
from __future__ import unicode_literals
-from django.conf import settings
from django.utils.translation import ugettext as _
from . import register
from spirit.forms.comment import CommentForm
-from spirit.models.comment import Comment, MOVED, CLOSED, UNCLOSED, PINNED, UNPINNED
-
-
-@register.assignment_tag()
-def get_comment_list(topic):
- return Comment.objects.for_topic(topic).order_by('date')
+from spirit.models.comment import MOVED, CLOSED, UNCLOSED, PINNED, UNPINNED
@register.inclusion_tag('spirit/comment/_form.html')
def render_comments_form(topic, next=None):
form = CommentForm()
- return {'form': form, 'topic_id': topic.pk, 'next': next, 'STATIC_URL': settings.STATIC_URL}
+ return {'form': form, 'topic_id': topic.pk, 'next': next}
@register.simple_tag()
diff --git a/spirit/templatetags/tags/comment_bookmark.py b/spirit/templatetags/tags/comment_bookmark.py
deleted file mode 100644
index 2dd86f4e1..000000000
--- a/spirit/templatetags/tags/comment_bookmark.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from __future__ import unicode_literals
-
-from . import register
-from spirit.models.comment_bookmark import CommentBookmark
-
-
-@register.simple_tag()
-def populate_bookmarks(topics, user, to_attr='bookmark'):
- bookmarks = CommentBookmark.objects.filter(topic__in=topics, user=user)\
- .select_related('topic')
- bookmarks_dict = {b.topic_id: b for b in bookmarks}
-
- for t in topics:
- setattr(t, to_attr, bookmarks_dict.get(t.pk))
-
- return ""
diff --git a/spirit/templatetags/tags/comment_like.py b/spirit/templatetags/tags/comment_like.py
index a2fee1062..62dbb9f1d 100644
--- a/spirit/templatetags/tags/comment_like.py
+++ b/spirit/templatetags/tags/comment_like.py
@@ -3,23 +3,10 @@
from __future__ import unicode_literals
from . import register
-from spirit.models.comment_like import CommentLike
-from spirit.forms.comment_like import LikeForm
+from ...forms.comment_like import LikeForm
@register.inclusion_tag('spirit/comment_like/_form.html')
def render_like_form(comment, like, next=None):
form = LikeForm()
return {'form': form, 'comment_id': comment.pk, 'like': like, 'next': next}
-
-
-@register.simple_tag()
-def populate_likes(comments, user, to_attr='like'):
- # TODO: use Prefetch on django 1.7, move this to CustomQuerySet.as_manager
- likes = CommentLike.objects.filter(comment__in=comments, user=user)
- likes_dict = {l.comment_id: l for l in likes}
-
- for c in comments:
- setattr(c, to_attr, likes_dict.get(c.pk))
-
- return ""
diff --git a/spirit/templatetags/tags/search.py b/spirit/templatetags/tags/search.py
index 78226b14b..69819c596 100644
--- a/spirit/templatetags/tags/search.py
+++ b/spirit/templatetags/tags/search.py
@@ -14,6 +14,7 @@ def render_search_form():
@register.assignment_tag()
def get_topics_from_search_result(results):
+ # TODO: move to view
# Since Im only indexing Topics this is ok.
topics = [r.object for r in results]
return topics
diff --git a/spirit/templatetags/tags/topic_notification.py b/spirit/templatetags/tags/topic_notification.py
index a31db0d03..41f85f47c 100644
--- a/spirit/templatetags/tags/topic_notification.py
+++ b/spirit/templatetags/tags/topic_notification.py
@@ -10,13 +10,12 @@
@register.assignment_tag()
def has_topic_notifications(user):
- return TopicNotification.objects.for_access(user=user)\
- .filter(is_read=False)\
- .exists()
+ return TopicNotification.objects.for_access(user=user).unread().exists()
@register.inclusion_tag('spirit/topic_notification/_form.html')
def render_notification_form(user, topic, next=None):
+ # TODO: remove form and use notification_activate and notification_deactivate ?
try:
notification = TopicNotification.objects.get(user=user, topic=topic)
except TopicNotification.DoesNotExist:
diff --git a/spirit/templatetags/tags/utils/avatar.py b/spirit/templatetags/tags/utils/avatar.py
new file mode 100644
index 000000000..fe0f02a64
--- /dev/null
+++ b/spirit/templatetags/tags/utils/avatar.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+import math
+
+from django.utils.encoding import smart_text
+
+from .. import register
+
+
+@register.simple_tag()
+def get_avatar_color(user):
+ # returns 0-215
+ return smart_text(int(215 * math.log10(user.id)))
diff --git a/spirit/templatetags/tags/utils/filters.py b/spirit/templatetags/tags/utils/filters.py
deleted file mode 100644
index 950d91914..000000000
--- a/spirit/templatetags/tags/utils/filters.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from __future__ import unicode_literals
-
-from .. import register
-
-
-@register.filter
-def has_errors(formset):
- """Checks if a FormSet has errors"""
- # TODO: test
- for form in formset:
- if form.errors:
- return True
-
- return False
diff --git a/spirit/templatetags/tags/utils/paginator.py b/spirit/templatetags/tags/utils/paginator.py
index a2194a676..8cbe8c5eb 100644
--- a/spirit/templatetags/tags/utils/paginator.py
+++ b/spirit/templatetags/tags/utils/paginator.py
@@ -2,28 +2,15 @@
from __future__ import unicode_literals
-from django.http import Http404
-from django.core.paginator import Paginator, InvalidPage
+from django.template.loader import render_to_string
+from django.core.paginator import Page
from .. import register
-from spirit.utils.paginator.yt_paginator import YTPaginator
+from spirit.utils.paginator import paginate, yt_paginate
-def _get_page(context, object_list, per_page, page_var, page_number, paginator_class):
- request = context["request"]
- page_number = page_number or request.GET.get(page_var, 1)
-
- paginator = paginator_class(object_list, per_page)
-
- try:
- page = paginator.page(page_number)
- except InvalidPage as err:
- raise Http404(err)
-
- return page
-
-
-def _render_paginator(context, page, page_var, hashtag):
+@register.simple_tag(takes_context=True)
+def render_paginator(context, page, page_var='page', hashtag=''):
query_dict = context["request"].GET.copy()
try:
@@ -39,29 +26,16 @@ def _render_paginator(context, page, page_var, hashtag):
if hashtag:
hashtag = "#%s" % hashtag
- return {
+ new_context = {
"page": page,
"page_var": page_var,
"hashtag": hashtag,
"extra_query": extra_query
}
+ if isinstance(page, Page):
+ template = "spirit/paginator/_paginator.html"
+ else:
+ template = "spirit/paginator/_yt_paginator.html"
-@register.assignment_tag(takes_context=True)
-def yt_paginator_autopaginate(context, object_list, per_page=15, page_var='page', page_number=None):
- return _get_page(context, object_list, per_page, page_var, page_number, YTPaginator)
-
-
-@register.inclusion_tag("spirit/paginator/_yt_paginator.html", takes_context=True)
-def render_yt_paginator(context, page, page_var='page', hashtag=''):
- return _render_paginator(context, page, page_var, hashtag)
-
-
-@register.assignment_tag(takes_context=True)
-def paginator_autopaginate(context, object_list, per_page=15, page_var='page', page_number=None):
- return _get_page(context, object_list, per_page, page_var, page_number, Paginator)
-
-
-@register.inclusion_tag("spirit/paginator/_paginator.html", takes_context=True)
-def render_paginator(context, page, page_var='page', hashtag=''):
- return _render_paginator(context, page, page_var, hashtag)
+ return render_to_string(template, new_context)
diff --git a/spirit/urls/__init__.py b/spirit/urls/__init__.py
index ec6dcc215..92e58dc31 100644
--- a/spirit/urls/__init__.py
+++ b/spirit/urls/__init__.py
@@ -4,28 +4,25 @@
from django.conf.urls import patterns, include, url
-import djconfig
-from spirit.forms.admin import BasicConfigForm
+urls = patterns('',
+ url(r'^$', 'spirit.views.topic.topic_active_list', name='index'),
+ url(r'^st/admin/', include('spirit.urls.admin')),
+ url(r'^category/', include('spirit.urls.category')),
+ url(r'^topic/', include('spirit.urls.topic')),
+ url(r'^topic/moderate/', include('spirit.urls.topic_moderate')),
+ url(r'^topic/unread/', include('spirit.urls.topic_unread')),
+ url(r'^topic/notification/', include('spirit.urls.topic_notification')),
+ url(r'^topic/favorite/', include('spirit.urls.topic_favorite')),
+ url(r'^topic/private/', include('spirit.urls.topic_private')),
+ url(r'^topic/poll/', include('spirit.urls.topic_poll')),
+ url(r'^comment/', include('spirit.urls.comment')),
+ url(r'^comment/bookmark/', include('spirit.urls.comment_bookmark')),
+ url(r'^comment/flag/', include('spirit.urls.comment_flag')),
+ url(r'^comment/history/', include('spirit.urls.comment_history')),
+ url(r'^comment/like/', include('spirit.urls.comment_like')),
+ url(r'^user/', include('spirit.urls.user')),
+ url(r'^search/', include('spirit.urls.search')),
+ )
-# TODO: use app loader in django 1.7
-djconfig.register(BasicConfigForm)
-
-urlpatterns = patterns('',
- url(r'^$', 'spirit.views.topic.topics_active', name='index'),
- url(r'^st/admin/', include('spirit.urls.admin')),
- url(r'^category/', include('spirit.urls.category')),
- url(r'^topic/', include('spirit.urls.topic')),
- url(r'^topic/unread/', include('spirit.urls.topic_unread')),
- url(r'^topic/notification/', include('spirit.urls.topic_notification')),
- url(r'^topic/favorite/', include('spirit.urls.topic_favorite')),
- url(r'^topic/private/', include('spirit.urls.topic_private')),
- url(r'^topic/poll/', include('spirit.urls.topic_poll')),
- url(r'^comment/', include('spirit.urls.comment')),
- url(r'^comment/bookmark/', include('spirit.urls.comment_bookmark')),
- url(r'^comment/flag/', include('spirit.urls.comment_flag')),
- url(r'^comment/history/', include('spirit.urls.comment_history')),
- url(r'^comment/like/', include('spirit.urls.comment_like')),
- url(r'^user/', include('spirit.urls.user')),
- url(r'^search/', include('spirit.urls.search')),
- )
+urlpatterns = patterns('', url(r'^', include(urls, namespace="spirit", app_name="spirit")))
diff --git a/spirit/urls/topic.py b/spirit/urls/topic.py
index 224d3aa15..98a9c6c1e 100644
--- a/spirit/urls/topic.py
+++ b/spirit/urls/topic.py
@@ -14,32 +14,5 @@
url(r'^(?P\d+)/$', 'topic_detail', kwargs={'slug': "", }, name='topic-detail'),
url(r'^(?P\d+)/(?P[\w-]+)/$', 'topic_detail', name='topic-detail'),
- url(r'^active/$', 'topics_active', name='topic-active'),
-
- url(r'^delete/(?P\d+)/$',
- 'topic_moderate',
- kwargs={'remove': True, 'value': True},
- name='topic-delete'),
- url(r'^undelete/(?P\d+)/$',
- 'topic_moderate',
- kwargs={'remove': True, 'value': False},
- name='topic-undelete'),
-
- url(r'^lock/(?P\d+)/$',
- 'topic_moderate',
- kwargs={'lock': True, 'value': True},
- name='topic-lock'),
- url(r'^unlock/(?P\d+)/$',
- 'topic_moderate',
- kwargs={'lock': True, 'value': False},
- name='topic-unlock'),
-
- url(r'^pin/(?P\d+)/$',
- 'topic_moderate',
- kwargs={'pin': True, 'value': True},
- name='topic-pin'),
- url(r'^unpin/(?P\d+)/$',
- 'topic_moderate',
- kwargs={'pin': True, 'value': False},
- name='topic-unpin'),
+ url(r'^active/$', 'topic_active_list', name='topic-active'),
)
diff --git a/spirit/urls/topic_moderate.py b/spirit/urls/topic_moderate.py
new file mode 100644
index 000000000..1c360f1ec
--- /dev/null
+++ b/spirit/urls/topic_moderate.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+from django.conf.urls import patterns, url
+
+from spirit.views.topic_moderate import TopicModerateDelete, TopicModerateUnDelete, \
+ TopicModerateLock, TopicModerateUnLock, TopicModeratePin, TopicModerateUnPin, \
+ TopicModerateGlobalPin, TopicModerateGlobalUnPin
+
+
+urlpatterns = patterns(
+ "spirit.views.topic_moderate",
+
+ url(r'^delete/(?P\d+)/$', TopicModerateDelete.as_view(), name='topic-delete'),
+ url(r'^undelete/(?P\d+)/$', TopicModerateUnDelete.as_view(), name='topic-undelete'),
+
+ url(r'^lock/(?P\d+)/$', TopicModerateLock.as_view(), name='topic-lock'),
+ url(r'^unlock/(?P\d+)/$', TopicModerateUnLock.as_view(), name='topic-unlock'),
+
+ url(r'^pin/(?P\d+)/$', TopicModeratePin.as_view(), name='topic-pin'),
+ url(r'^unpin/(?P\d+)/$', TopicModerateUnPin.as_view(), name='topic-unpin'),
+
+ url(r'^globallypin/(?P\d+)/$', TopicModerateGlobalPin.as_view(), name='topic-global-pin'),
+ url(r'^ungloballypin/(?P\d+)/$', TopicModerateGlobalUnPin.as_view(), name='topic-global-unpin'),
+ )
diff --git a/spirit/urls/user.py b/spirit/urls/user.py
index 461438e86..856b224ae 100644
--- a/spirit/urls/user.py
+++ b/spirit/urls/user.py
@@ -3,8 +3,10 @@
from __future__ import unicode_literals
from django.conf.urls import patterns, url
+from django.contrib.auth.views import (password_reset_done,
+ password_reset_confirm,
+ password_reset_complete)
from django.core.urlresolvers import reverse_lazy
-from django.contrib.auth.views import *
urlpatterns = patterns('spirit.views.user',
diff --git a/spirit/utils/decorators.py b/spirit/utils/decorators.py
index 3792f9d0b..fc06988c0 100644
--- a/spirit/utils/decorators.py
+++ b/spirit/utils/decorators.py
@@ -5,6 +5,7 @@
from django.core.exceptions import PermissionDenied
from django.contrib.auth.views import redirect_to_login
+from django.shortcuts import redirect
from django.conf import settings
@@ -40,3 +41,15 @@ def wrapper(request, *args, **kwargs):
return view_func(request, *args, **kwargs)
return wrapper
+
+
+def guest_only(view_func):
+ # TODO: test!
+ @wraps(view_func)
+ def wrapper(request, *args, **kwargs):
+ if request.user.is_authenticated():
+ return redirect(request.GET.get('next', request.user.get_absolute_url()))
+
+ return view_func(request, *args, **kwargs)
+
+ return wrapper
\ No newline at end of file
diff --git a/spirit/utils/forms.py b/spirit/utils/forms.py
index 0b956c4e4..5b7a3489d 100644
--- a/spirit/utils/forms.py
+++ b/spirit/utils/forms.py
@@ -4,6 +4,7 @@
from django import forms
from django.utils.encoding import smart_text
+from django.db.models import Prefetch
class NestedModelChoiceField(forms.ModelChoiceField):
@@ -27,7 +28,7 @@ def _populate_choices(self):
choices = [("", self.empty_label), ]
kwargs = {self.parent_field: None, }
queryset = self.queryset.filter(**kwargs)\
- .prefetch_related(self.related_name)
+ .prefetch_related(Prefetch(self.related_name, queryset=self.queryset))
for parent in queryset:
choices.append((self.prepare_value(parent), self.label_from_instance(parent)))
diff --git a/spirit/utils/markdown/inline.py b/spirit/utils/markdown/inline.py
index 2c8cca100..fab08ade3 100644
--- a/spirit/utils/markdown/inline.py
+++ b/spirit/utils/markdown/inline.py
@@ -8,6 +8,7 @@
from django.conf import settings
from django.contrib.auth import get_user_model
+from django.contrib.staticfiles.storage import staticfiles_storage
import mistune
@@ -59,7 +60,8 @@ def output_emoji(self, m):
return m.group(0)
image = emoji + '.png'
- path = os.path.join(settings.STATIC_URL, 'spirit', 'emojis', image).replace('\\', '/')
+ rel_path = os.path.join('spirit', 'emojis', image).replace('\\', '/')
+ path = staticfiles_storage.url(rel_path)
return self.renderer.emoji(path)
diff --git a/spirit/utils/paginator/__init__.py b/spirit/utils/paginator/__init__.py
index ae24a8e67..fd087204b 100644
--- a/spirit/utils/paginator/__init__.py
+++ b/spirit/utils/paginator/__init__.py
@@ -3,6 +3,10 @@
from __future__ import unicode_literals
from django.utils.http import urlencode
+from django.http import Http404
+from django.core.paginator import Paginator, InvalidPage
+
+from spirit.utils.paginator.yt_paginator import YTPaginator
def get_page_number(obj_number, per_page):
@@ -22,3 +26,21 @@ def get_url(url, obj_number, per_page, page_var):
return "".join((url, '#c', str(obj_number)))
return "".join((url, '?', data, '#c', str(obj_number)))
+
+
+def _paginate(paginator_class, object_list, per_page=15, page_number=None):
+ page_number = page_number or 1
+ paginator = paginator_class(object_list, per_page)
+
+ try:
+ return paginator.page(page_number)
+ except InvalidPage as err:
+ raise Http404(err)
+
+
+def paginate(*args, **kwargs):
+ return _paginate(Paginator, *args, **kwargs)
+
+
+def yt_paginate(*args, **kwargs):
+ return _paginate(YTPaginator, *args, **kwargs)
diff --git a/spirit/utils/paginator/infinite_paginator.py b/spirit/utils/paginator/infinite_paginator.py
index 0debe2a49..a277d99ca 100644
--- a/spirit/utils/paginator/infinite_paginator.py
+++ b/spirit/utils/paginator/infinite_paginator.py
@@ -8,6 +8,7 @@
def paginate(request, query_set, lookup_field, per_page=15, page_var='value'):
+ # TODO: remove
page_pk = request.GET.get(page_var, None)
paginator = SeekPaginator(query_set, per_page=per_page, lookup_field=lookup_field)
diff --git a/spirit/utils/paginator/yt_paginator.py b/spirit/utils/paginator/yt_paginator.py
index c1d43f7f9..0a2d44a03 100644
--- a/spirit/utils/paginator/yt_paginator.py
+++ b/spirit/utils/paginator/yt_paginator.py
@@ -8,10 +8,6 @@
class YTPaginator(object):
"""
- Paginator for efficiently paginating large object collections on systems
- where using standard Django pagination is impractical because of significant
- ``count(*)`` query overhead.
-
It'll limit the page list to a given limit
"""
@@ -35,9 +31,6 @@ def validate_number(self, number):
def page(self, number):
"""
Returns a Page object for the given 1-based page number.
-
- Retrieves objects for the given page number plus 1 additional to check
- if there are more objects after this page.
"""
number = self.validate_number(number)
offset = (number - 1) * self.per_page
@@ -64,6 +57,9 @@ def __init__(self, object_list, number, paginator):
def __repr__(self):
return '' % self.number
+ def __len__(self):
+ return len(self.object_list)
+
def __getitem__(self, index):
if not isinstance(self.object_list, list):
self.object_list = list(self.object_list)
diff --git a/spirit/utils/user/tokens.py b/spirit/utils/user/tokens.py
index c4201cf61..da9c14e7d 100644
--- a/spirit/utils/user/tokens.py
+++ b/spirit/utils/user/tokens.py
@@ -38,8 +38,7 @@ def is_valid(self, user, signed_value):
class UserActivationTokenGenerator(TokenGenerator):
def _uid(self, user):
- # Older mysql won't store ms.
- return ";".join((smart_text(user.pk), smart_text(user.last_login.replace(microsecond=0))))
+ return ";".join((smart_text(user.pk), smart_text(user.is_verified)))
class UserEmailChangeTokenGenerator(TokenGenerator):
diff --git a/spirit/views/admin/category.py b/spirit/views/admin/category.py
index 6b313f41d..63971d638 100644
--- a/spirit/views/admin/category.py
+++ b/spirit/views/admin/category.py
@@ -20,7 +20,8 @@
@administrator_required
def category_list(request):
categories = Category.objects.filter(parent=None, is_private=False)
- return render(request, 'spirit/admin/category/category_list.html', {'categories': categories, })
+ context = {'categories': categories, }
+ return render(request, 'spirit/admin/category/category_list.html', context)
@administrator_required
@@ -34,7 +35,9 @@ def category_create(request):
else:
form = CategoryForm()
- return render(request, 'spirit/admin/category/category_create.html', {'form': form, })
+ context = {'form': form, }
+
+ return render(request, 'spirit/admin/category/category_create.html', context)
@administrator_required
@@ -51,4 +54,6 @@ def category_update(request, category_id):
else:
form = CategoryForm(instance=category)
- return render(request, 'spirit/admin/category/category_update.html', {'form': form, })
+ context = {'form': form, }
+
+ return render(request, 'spirit/admin/category/category_update.html', context)
diff --git a/spirit/views/admin/comment_flag.py b/spirit/views/admin/comment_flag.py
index b5aea2fef..ea52b9d31 100644
--- a/spirit/views/admin/comment_flag.py
+++ b/spirit/views/admin/comment_flag.py
@@ -7,22 +7,35 @@
from django.contrib import messages
from django.utils.translation import ugettext as _
-from spirit.utils.decorators import administrator_required
+from djconfig import config
-from spirit.models.comment_flag import CommentFlag, Flag
-from spirit.forms.admin import CommentFlagForm
+from ...utils.paginator import yt_paginate
+from ...utils.decorators import administrator_required
+
+from ...models.comment_flag import CommentFlag, Flag
+from ...forms.admin import CommentFlagForm
@administrator_required
def flag_open(request):
- flags = CommentFlag.objects.filter(is_closed=False)
- return render(request, 'spirit/admin/comment_flag/flag_open.html', {'flags': flags, })
+ flags = yt_paginate(
+ CommentFlag.objects.filter(is_closed=False),
+ per_page=config.comments_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+ context = {'flags': flags, }
+ return render(request, 'spirit/admin/comment_flag/flag_open.html', context)
@administrator_required
def flag_closed(request):
- flags = CommentFlag.objects.filter(is_closed=True)
- return render(request, 'spirit/admin/comment_flag/flag_closed.html', {'flags': flags, })
+ flags = yt_paginate(
+ CommentFlag.objects.filter(is_closed=True),
+ per_page=config.comments_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+ context = {'flags': flags, }
+ return render(request, 'spirit/admin/comment_flag/flag_closed.html', context)
@administrator_required
@@ -39,7 +52,16 @@ def flag_detail(request, pk):
else:
form = CommentFlagForm(instance=flag)
- flags = Flag.objects.filter(comment=flag.comment)
- return render(request, 'spirit/admin/comment_flag/flag_detail.html', {'flag': flag,
- 'flags': flags,
- 'form': form})
+ flags = yt_paginate(
+ Flag.objects.filter(comment=flag.comment),
+ per_page=config.comments_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+
+ context = {
+ 'flag': flag,
+ 'flags': flags,
+ 'form': form
+ }
+
+ return render(request, 'spirit/admin/comment_flag/flag_detail.html', context)
diff --git a/spirit/views/admin/config.py b/spirit/views/admin/config.py
index 7a3c0c4d8..e521d453c 100644
--- a/spirit/views/admin/config.py
+++ b/spirit/views/admin/config.py
@@ -24,4 +24,6 @@ def config_basic(request):
else:
form = BasicConfigForm()
- return render(request, 'spirit/admin/config/config_basic.html', {'form': form, })
+ context = {'form': form, }
+
+ return render(request, 'spirit/admin/config/config_basic.html', context)
diff --git a/spirit/views/admin/index.py b/spirit/views/admin/index.py
index 171022bbd..53c1ea2c4 100644
--- a/spirit/views/admin/index.py
+++ b/spirit/views/admin/index.py
@@ -21,17 +21,14 @@
@administrator_required
def dashboard(request):
# Strongly unaccurate counters below...
- category_count = Category.objects.all().count() - 1 # - private
- topics_count = Topic.objects.all().count()
- comments_count = Comment.objects.all().count()
- users_count = User.objects.all().count()
- flags_count = CommentFlag.objects.filter(is_closed=False).count()
- likes_count = CommentLike.objects.all().count()
-
- return render(request, 'spirit/admin/index/dashboard.html', {'version': spirit.__version__,
- 'category_count': category_count,
- 'topics_count': topics_count,
- 'comments_count': comments_count,
- 'users_count': users_count,
- 'flags_count': flags_count,
- 'likes_count': likes_count})
+ context = {
+ 'version': spirit.__version__,
+ 'category_count': Category.objects.all().count() - 1, # - private
+ 'topics_count': Topic.objects.all().count(),
+ 'comments_count': Comment.objects.all().count(),
+ 'users_count': User.objects.all().count(),
+ 'flags_count': CommentFlag.objects.filter(is_closed=False).count(),
+ 'likes_count': CommentLike.objects.all().count()
+ }
+
+ return render(request, 'spirit/admin/index/dashboard.html', context)
diff --git a/spirit/views/admin/topic.py b/spirit/views/admin/topic.py
index 5cc71762f..fc4490c91 100644
--- a/spirit/views/admin/topic.py
+++ b/spirit/views/admin/topic.py
@@ -4,6 +4,9 @@
from django.shortcuts import render
+from djconfig import config
+
+from ...utils.paginator import yt_paginate
from spirit.utils.decorators import administrator_required
from spirit.models.topic import Topic
@@ -11,17 +14,33 @@
@administrator_required
def topic_deleted(request):
# Private topics cant be deleted, closed or pinned so we are ok
- topics = Topic.objects.filter(is_removed=True)
- return render(request, 'spirit/admin/topic/topic_deleted.html', {'topics': topics, })
+ topics = yt_paginate(
+ Topic.objects.filter(is_removed=True),
+ per_page=config.topics_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+ context = {'topics': topics, }
+ return render(request, 'spirit/admin/topic/topic_deleted.html', context)
@administrator_required
def topic_closed(request):
- topics = Topic.objects.filter(is_closed=True)
- return render(request, 'spirit/admin/topic/topic_closed.html', {'topics': topics, })
+ topics = yt_paginate(
+ Topic.objects.filter(is_closed=True),
+ per_page=config.topics_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+ context = {'topics': topics, }
+ return render(request, 'spirit/admin/topic/topic_closed.html', context)
@administrator_required
def topic_pinned(request):
- topics = Topic.objects.filter(is_pinned=True)
- return render(request, 'spirit/admin/topic/topic_pinned.html', {'topics': topics, })
+ topics = Topic.objects.filter(is_pinned=True) | Topic.objects.filter(is_globally_pinned=True)
+ topics = yt_paginate(
+ topics,
+ per_page=config.topics_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+ context = {'topics': topics, }
+ return render(request, 'spirit/admin/topic/topic_pinned.html', context)
diff --git a/spirit/views/admin/user.py b/spirit/views/admin/user.py
index c3745a799..f18ede6f5 100644
--- a/spirit/views/admin/user.py
+++ b/spirit/views/admin/user.py
@@ -7,6 +7,9 @@
from django.contrib import messages
from django.utils.translation import ugettext as _
+from djconfig import config
+
+from ...utils.paginator import yt_paginate
from spirit.utils.decorators import administrator_required
from spirit.forms.admin import UserEditForm
@@ -29,28 +32,50 @@ def user_edit(request, user_id):
else:
form = UserEditForm(instance=user)
- return render(request, 'spirit/admin/user/user_edit.html', {'form': form, })
+ context = {'form': form, }
+
+ return render(request, 'spirit/admin/user/user_edit.html', context)
@administrator_required
def user_list(request):
- users = User.objects.all()
- return render(request, 'spirit/admin/user/user_list.html', {'users': users, })
+ users = yt_paginate(
+ User.objects.all(),
+ per_page=config.topics_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+ context = {'users': users, }
+ return render(request, 'spirit/admin/user/user_list.html', context)
@administrator_required
def user_admins(request):
- users = User.objects.filter(is_administrator=True)
- return render(request, 'spirit/admin/user/user_admins.html', {'users': users, })
+ users = yt_paginate(
+ User.objects.filter(is_administrator=True),
+ per_page=config.topics_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+ context = {'users': users, }
+ return render(request, 'spirit/admin/user/user_admins.html', context)
@administrator_required
def user_mods(request):
- users = User.objects.filter(is_moderator=True, is_administrator=False)
- return render(request, 'spirit/admin/user/user_mods.html', {'users': users, })
+ users = yt_paginate(
+ User.objects.filter(is_moderator=True, is_administrator=False),
+ per_page=config.topics_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+ context = {'users': users, }
+ return render(request, 'spirit/admin/user/user_mods.html', context)
@administrator_required
def user_unactive(request):
- users = User.objects.filter(is_active=False)
- return render(request, 'spirit/admin/user/user_unactive.html', {'users': users, })
+ users = yt_paginate(
+ User.objects.filter(is_active=False),
+ per_page=config.topics_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+ context = {'users': users, }
+ return render(request, 'spirit/admin/user/user_unactive.html', context)
diff --git a/spirit/views/category.py b/spirit/views/category.py
index b933bc085..8d3488735 100644
--- a/spirit/views/category.py
+++ b/spirit/views/category.py
@@ -5,30 +5,51 @@
from django.views.generic import ListView
from django.shortcuts import render
from django.http import HttpResponsePermanentRedirect
+from django.shortcuts import get_object_or_404
+from djconfig import config
+
+from ..utils.paginator import yt_paginate
from spirit.models.topic import Topic
from spirit.models.category import Category
def category_detail(request, pk, slug):
- category = Category.objects.get_public_or_404(pk=pk)
+ category = get_object_or_404(Category.objects.visible(),
+ pk=pk)
if category.slug != slug:
return HttpResponsePermanentRedirect(category.get_absolute_url())
- subcategories = Category.objects.for_parent(parent=category)
- topics = Topic.objects.for_category(category=category)\
- .order_by('-is_pinned', '-last_active')\
+ subcategories = Category.objects\
+ .visible()\
+ .children(parent=category)
+
+ topics = Topic.objects\
+ .unremoved()\
+ .with_bookmarks(user=request.user)\
+ .for_category(category=category)\
+ .order_by('-is_globally_pinned', '-is_pinned', '-last_active')\
.select_related('category')
- return render(request, 'spirit/category/category_detail.html', {'category': category,
- 'subcategories': subcategories,
- 'topics': topics})
+ topics = yt_paginate(
+ topics,
+ per_page=config.topics_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+
+ context = {
+ 'category': category,
+ 'subcategories': subcategories,
+ 'topics': topics
+ }
+
+ return render(request, 'spirit/category/category_detail.html', context)
class CategoryList(ListView):
template_name = 'spirit/category/category_list.html'
context_object_name = "categories"
- queryset = Category.objects.for_parent()
+ queryset = Category.objects.visible().parents()
diff --git a/spirit/views/comment.py b/spirit/views/comment.py
index 9fc99fe83..dcefa86ad 100644
--- a/spirit/views/comment.py
+++ b/spirit/views/comment.py
@@ -9,21 +9,23 @@
from django.conf import settings
from django.http import Http404
-from spirit.utils.ratelimit.decorators import ratelimit
-from spirit.models.topic import Topic
-from spirit.utils import paginator, markdown
-from spirit.utils.decorators import moderator_required
-from spirit.utils import json_response, render_form_errors
+from djconfig import config
-from spirit.models.comment import Comment
-from spirit.forms.comment import CommentForm, CommentMoveForm, CommentImageForm
-from spirit.signals.comment import comment_posted, comment_pre_update, comment_post_update, comment_moved
+from ..utils.ratelimit.decorators import ratelimit
+from ..models.topic import Topic
+from ..utils import paginator, markdown
+from ..utils.decorators import moderator_required
+from ..utils import json_response, render_form_errors
+
+from ..models.comment import Comment
+from ..forms.comment import CommentForm, CommentMoveForm, CommentImageForm
+from ..signals.comment import comment_posted, comment_pre_update, comment_post_update, comment_moved
@login_required
@ratelimit(rate='1/10s')
def comment_publish(request, topic_id, pk=None):
- topic = get_object_or_404(Topic.objects.for_access_open(request.user),
+ topic = get_object_or_404(Topic.objects.opened().for_access(request.user),
pk=topic_id)
if request.method == 'POST':
@@ -37,13 +39,18 @@ def comment_publish(request, topic_id, pk=None):
initial = None
if pk:
- comment = get_object_or_404(Comment.objects.for_all(), pk=pk)
+ comment = get_object_or_404(Comment.objects.for_access(user=request.user), pk=pk)
quote = markdown.quotify(comment.comment, comment.user.username)
initial = {'comment': quote, }
form = CommentForm(initial=initial)
- return render(request, 'spirit/comment/comment_publish.html', {'form': form, 'topic': topic})
+ context = {
+ 'form': form,
+ 'topic': topic
+ }
+
+ return render(request, 'spirit/comment/comment_publish.html', context)
@login_required
@@ -61,7 +68,9 @@ def comment_update(request, pk):
else:
form = CommentForm(instance=comment)
- return render(request, 'spirit/comment/comment_update.html', {'form': form, })
+ context = {'form': form, }
+
+ return render(request, 'spirit/comment/comment_update.html', context)
@moderator_required
@@ -74,7 +83,9 @@ def comment_delete(request, pk, remove=True):
return redirect(comment.get_absolute_url())
- return render(request, 'spirit/comment/comment_moderate.html', {'comment': comment, })
+ context = {'comment': comment, }
+
+ return render(request, 'spirit/comment/comment_moderate.html', context)
@require_POST
@@ -101,8 +112,8 @@ def comment_find(request, pk):
comment_number = Comment.objects.filter(topic=comment.topic, date__lte=comment.date).count()
url = paginator.get_url(comment.topic.get_absolute_url(),
comment_number,
- settings.ST_COMMENTS_PER_PAGE,
- settings.ST_COMMENTS_PAGE_VAR)
+ config.comments_per_page,
+ 'page')
return redirect(url)
diff --git a/spirit/views/comment_flag.py b/spirit/views/comment_flag.py
index f5e6063d3..c85b63bd1 100644
--- a/spirit/views/comment_flag.py
+++ b/spirit/views/comment_flag.py
@@ -23,4 +23,9 @@ def flag_create(request, comment_id):
else:
form = FlagForm()
- return render(request, 'spirit/comment_flag/flag_create.html', {'form': form, 'comment': comment})
+ context = {
+ 'form': form,
+ 'comment': comment
+ }
+
+ return render(request, 'spirit/comment_flag/flag_create.html', context)
diff --git a/spirit/views/comment_history.py b/spirit/views/comment_history.py
index 2744f1c0e..540a5a22c 100644
--- a/spirit/views/comment_history.py
+++ b/spirit/views/comment_history.py
@@ -5,13 +5,29 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, get_object_or_404
+from djconfig import config
+
+from ..utils.paginator import yt_paginate
from ..models.comment_history import CommentHistory
from ..models.comment import Comment
@login_required
def comment_history_detail(request, comment_id):
- comment = get_object_or_404(Comment.objects.for_access(request.user), pk=comment_id)
- comments = CommentHistory.objects.filter(comment_fk=comment)\
- .order_by('date')
- return render(request, 'spirit/comment_history/detail.html', {'comments': comments, })
+ comment = get_object_or_404(Comment.objects.for_access(request.user),
+ pk=comment_id)
+
+ comments = CommentHistory.objects\
+ .filter(comment_fk=comment)\
+ .select_related('comment_fk__user')\
+ .order_by('date', 'pk')
+
+ comments = yt_paginate(
+ comments,
+ per_page=config.comments_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+
+ context = {'comments': comments, }
+
+ return render(request, 'spirit/comment_history/detail.html', context)
diff --git a/spirit/views/comment_like.py b/spirit/views/comment_like.py
index 0eec290f4..efe24e108 100644
--- a/spirit/views/comment_like.py
+++ b/spirit/views/comment_like.py
@@ -32,7 +32,12 @@ def like_create(request, comment_id):
else:
form = LikeForm()
- return render(request, 'spirit/comment_like/like_create.html', {'form': form, 'comment': comment})
+ context = {
+ 'form': form,
+ 'comment': comment
+ }
+
+ return render(request, 'spirit/comment_like/like_create.html', context)
@login_required
@@ -44,9 +49,11 @@ def like_delete(request, pk):
comment_like_post_delete.send(sender=like.__class__, comment=like.comment)
if request.is_ajax():
- return json_response({'url_create': reverse('spirit:like-create',
- kwargs={'comment_id': like.comment.pk, }), })
+ url = reverse('spirit:like-create', kwargs={'comment_id': like.comment.pk, })
+ return json_response({'url_create': url, })
return redirect(request.POST.get('next', like.comment.get_absolute_url()))
- return render(request, 'spirit/comment_like/like_delete.html', {'like': like, })
+ context = {'like': like, }
+
+ return render(request, 'spirit/comment_like/like_delete.html', context)
diff --git a/spirit/views/search.py b/spirit/views/search.py
index c2e7fef40..1fea0c91b 100644
--- a/spirit/views/search.py
+++ b/spirit/views/search.py
@@ -4,10 +4,18 @@
from haystack.views import SearchView as BaseSearchView
+from djconfig import config
+
+from ..utils.paginator import yt_paginate
+
class SearchView(BaseSearchView):
def build_page(self):
paginator = None
- page = self.results
+ page = yt_paginate(
+ self.results,
+ per_page=config.topics_per_page,
+ page_number=self.request.GET.get('page', 1)
+ )
return paginator, page
diff --git a/spirit/views/topic.py b/spirit/views/topic.py
index 798ab63ea..5c8b19a0c 100644
--- a/spirit/views/topic.py
+++ b/spirit/views/topic.py
@@ -5,26 +5,30 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponsePermanentRedirect
-from django.conf import settings
-from spirit.utils.ratelimit.decorators import ratelimit
-from spirit.utils.decorators import moderator_required
-from spirit.models.category import Category
-from spirit.models.comment import MOVED, CLOSED, UNCLOSED, PINNED, UNPINNED
-from spirit.forms.comment import CommentForm
-from spirit.signals.comment import comment_posted
-from spirit.forms.topic_poll import TopicPollForm, TopicPollChoiceFormSet
+from djconfig import config
-from spirit.models.topic import Topic
-from spirit.forms.topic import TopicForm
-from spirit.signals.topic import topic_viewed, topic_post_moderate
+from ..utils.paginator import paginate, yt_paginate
+from ..utils.ratelimit.decorators import ratelimit
+from ..models.category import Category
+from ..models.comment import MOVED
+from ..forms.comment import CommentForm
+from ..signals.comment import comment_posted
+from ..forms.topic_poll import TopicPollForm, TopicPollChoiceFormSet
+
+from ..models.comment import Comment
+from ..models.topic import Topic
+from ..forms.topic import TopicForm
+from ..signals.topic import topic_viewed
+from ..signals.topic_moderate import topic_post_moderate
@login_required
@ratelimit(rate='1/10s')
def topic_publish(request, category_id=None):
if category_id:
- Category.objects.get_public_or_404(pk=category_id)
+ get_object_or_404(Category.objects.visible(),
+ pk=category_id)
if request.method == 'POST':
form = TopicForm(user=request.user, data=request.POST)
@@ -55,8 +59,14 @@ def topic_publish(request, category_id=None):
pform = TopicPollForm()
pformset = TopicPollChoiceFormSet(can_delete=False)
- return render(request, 'spirit/topic/topic_publish.html', {'form': form, 'cform': cform,
- 'pform': pform, 'pformset': pformset})
+ context = {
+ 'form': form,
+ 'cform': cform,
+ 'pform': pform,
+ 'pformset': pformset
+ }
+
+ return render(request, 'spirit/topic/topic_publish.html', context)
@login_required
@@ -77,7 +87,9 @@ def topic_update(request, pk):
else:
form = TopicForm(user=request.user, instance=topic)
- return render(request, 'spirit/topic/topic_update.html', {'form': form, })
+ context = {'form': form, }
+
+ return render(request, 'spirit/topic/topic_update.html', context)
def topic_detail(request, pk, slug):
@@ -88,51 +100,45 @@ def topic_detail(request, pk, slug):
topic_viewed.send(sender=topic.__class__, request=request, topic=topic)
- return render(request, 'spirit/topic/topic_detail.html', {'topic': topic,
- 'COMMENTS_PER_PAGE': settings.ST_COMMENTS_PER_PAGE})
-
-
-@moderator_required
-def topic_moderate(request, pk, value, remove=False, lock=False, pin=False):
- # TODO: move to topic_moderate and split it in many views
- topic = get_object_or_404(Topic, pk=pk)
-
- if request.method == 'POST':
- not_value = not value
-
- if remove:
- Topic.objects.filter(pk=pk, is_removed=not_value)\
- .update(is_removed=value)
+ comments = Comment.objects\
+ .for_topic(topic=topic)\
+ .with_likes(user=request.user)\
+ .order_by('date')
- if lock:
- count = Topic.objects.filter(pk=pk, is_closed=not_value)\
- .update(is_closed=value)
+ comments = paginate(
+ comments,
+ per_page=config.comments_per_page,
+ page_number=request.GET.get('page', 1)
+ )
- if count:
- action = CLOSED if value else UNCLOSED
- topic_post_moderate.send(sender=topic.__class__, user=request.user, topic=topic, action=action)
+ context = {
+ 'topic': topic,
+ 'comments': comments
+ }
- if pin:
- count = Topic.objects.filter(pk=pk, is_pinned=not_value)\
- .update(is_pinned=value)
+ return render(request, 'spirit/topic/topic_detail.html', context)
- if count:
- action = PINNED if value else UNPINNED
- topic_post_moderate.send(sender=topic.__class__, user=request.user, topic=topic, action=action)
- return redirect(request.POST.get('next', topic.get_absolute_url()))
+def topic_active_list(request):
+ categories = Category.objects\
+ .visible()\
+ .parents()
- return render(request, 'spirit/topic/topic_moderate.html', {'topic': topic, })
+ topics = Topic.objects\
+ .visible()\
+ .with_bookmarks(user=request.user)\
+ .order_by('-is_globally_pinned', '-last_active')\
+ .select_related('category')
+ topics = yt_paginate(
+ topics,
+ per_page=config.topics_per_page,
+ page_number=request.GET.get('page', 1)
+ )
-def topics_active(request):
- topics = Topic.objects.for_public().filter(is_pinned=False)
- topics_pinned = Topic.objects.filter(category_id=settings.ST_UNCATEGORIZED_CATEGORY_PK,
- is_removed=False,
- is_pinned=True)
- topics = topics | topics_pinned
- topics = topics.order_by('-is_pinned', '-last_active').select_related('category')
- categories = Category.objects.for_parent()
+ context = {
+ 'categories': categories,
+ 'topics': topics
+ }
- return render(request, 'spirit/topic/topics_active.html', {'categories': categories,
- 'topics': topics})
+ return render(request, 'spirit/topic/topics_active.html', context)
diff --git a/spirit/views/topic_moderate.py b/spirit/views/topic_moderate.py
new file mode 100644
index 000000000..6655af298
--- /dev/null
+++ b/spirit/views/topic_moderate.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+from django.shortcuts import render, redirect, get_object_or_404
+from django.views.generic import View
+from django.utils.decorators import method_decorator
+
+from spirit.utils.decorators import moderator_required
+from spirit.models.comment import CLOSED, UNCLOSED, PINNED, UNPINNED
+
+from spirit.models.topic import Topic
+from spirit.signals.topic_moderate import topic_post_moderate
+
+
+class TopicModerateBase(View):
+
+ action = None
+ field_name = None
+ to_value = None # bool
+
+ def update(self, pk):
+ not_value = not self.to_value
+ return Topic.objects\
+ .filter(**{'pk': pk, self.field_name: not_value})\
+ .update(**{self.field_name: self.to_value, })
+
+ def send_signal(self, user, action):
+ topic_post_moderate.send(sender=self.topic.__class__, user=user,
+ topic=self.topic, action=action)
+
+ def post(self, request, *args, **kwargs):
+ pk = kwargs['pk']
+ count = self.update(pk)
+
+ if count and self.action is not None:
+ self.send_signal(request.user, self.action)
+
+ return redirect(request.POST.get('next', self.topic.get_absolute_url()))
+
+ def get(self, request, *args, **kwargs):
+ return render(request, 'spirit/topic/topic_moderate.html', {'topic': self.topic, })
+
+ def check_configuration(self):
+ assert self.field_name is not None, "You forgot to set field_name attribute"
+ assert self.to_value is not None, "You forgot to set to_value attribute"
+
+ @method_decorator(moderator_required)
+ def dispatch(self, *args, **kwargs):
+ self.check_configuration()
+ self.topic = get_object_or_404(Topic, pk=kwargs['pk'])
+ return super(TopicModerateBase, self).dispatch(*args, **kwargs)
+
+
+class TopicModerateDelete(TopicModerateBase):
+
+ field_name = 'is_removed'
+ to_value = True
+
+
+class TopicModerateUnDelete(TopicModerateBase):
+
+ field_name = 'is_removed'
+ to_value = False
+
+
+class TopicModerateLock(TopicModerateBase):
+
+ action = CLOSED
+ field_name = 'is_closed'
+ to_value = True
+
+
+class TopicModerateUnLock(TopicModerateBase):
+
+ action = UNCLOSED
+ field_name = 'is_closed'
+ to_value = False
+
+
+class TopicModeratePin(TopicModerateBase):
+
+ action = PINNED
+ field_name = 'is_pinned'
+ to_value = True
+
+
+class TopicModerateUnPin(TopicModerateBase):
+
+ action = UNPINNED
+ field_name = 'is_pinned'
+ to_value = False
+
+
+class TopicModerateGlobalPin(TopicModerateBase):
+
+ action = PINNED
+ field_name = 'is_globally_pinned'
+ to_value = True
+
+
+class TopicModerateGlobalUnPin(TopicModerateBase):
+
+ action = UNPINNED
+ field_name = 'is_globally_pinned'
+ to_value = False
diff --git a/spirit/views/topic_notification.py b/spirit/views/topic_notification.py
index 5625c95e3..18ec740c1 100644
--- a/spirit/views/topic_notification.py
+++ b/spirit/views/topic_notification.py
@@ -11,12 +11,15 @@
from django.conf import settings
from django.contrib import messages
-from spirit import utils
-from spirit.models.topic import Topic
-from spirit.utils.paginator.infinite_paginator import paginate
+from djconfig import config
-from spirit.models.topic_notification import TopicNotification
-from spirit.forms.topic_notification import NotificationForm, NotificationCreationForm
+from .. import utils
+from ..models.topic import Topic
+from ..utils.paginator import yt_paginate
+from ..utils.paginator.infinite_paginator import paginate
+
+from ..models.topic_notification import TopicNotification
+from ..forms.topic_notification import NotificationForm, NotificationCreationForm
@require_POST
@@ -28,10 +31,10 @@ def notification_create(request, topic_id):
if form.is_valid():
form.save()
- return redirect(request.POST.get('next', topic.get_absolute_url()))
else:
messages.error(request, utils.render_form_errors(form))
- return redirect(request.POST.get('next', topic.get_absolute_url()))
+
+ return redirect(request.POST.get('next', topic.get_absolute_url()))
@require_POST
@@ -42,10 +45,10 @@ def notification_update(request, pk):
if form.is_valid():
form.save()
- return redirect(request.POST.get('next', notification.topic.get_absolute_url()))
else:
messages.error(request, utils.render_form_errors(form))
- return redirect(request.POST.get('next', notification.topic.get_absolute_url()))
+
+ return redirect(request.POST.get('next', notification.topic.get_absolute_url()))
@login_required
@@ -53,9 +56,12 @@ def notification_ajax(request):
if not request.is_ajax():
return Http404()
- notifications = TopicNotification.objects.for_access(request.user)\
+ notifications = TopicNotification.objects\
+ .for_access(request.user)\
.order_by("is_read", "-date")\
- .select_related('comment__user', 'comment__topic')[:settings.ST_NOTIFICATIONS_PER_PAGE]
+ .select_related('comment__user', 'comment__topic')
+
+ notifications = notifications[:settings.ST_NOTIFICATIONS_PER_PAGE]
notifications = [{'user': n.comment.user.username, 'action': n.action,
'title': n.comment.topic.title, 'url': n.get_absolute_url(),
@@ -67,7 +73,8 @@ def notification_ajax(request):
@login_required
def notification_list_unread(request):
- notifications = TopicNotification.objects.for_access(request.user)\
+ notifications = TopicNotification.objects\
+ .for_access(request.user)\
.filter(is_read=False)
page = paginate(request, query_set=notifications, lookup_field="date",
@@ -77,11 +84,22 @@ def notification_list_unread(request):
if page:
next_page_pk = page[-1].pk
- return render(request, 'spirit/topic_notification/list_unread.html', {'page': page,
- 'next_page_pk': next_page_pk})
+ context = {
+ 'page': page,
+ 'next_page_pk': next_page_pk
+ }
+
+ return render(request, 'spirit/topic_notification/list_unread.html', context)
@login_required
def notification_list(request):
- notifications = TopicNotification.objects.for_access(request.user)
- return render(request, 'spirit/topic_notification/list.html', {'notifications': notifications, })
+ notifications = yt_paginate(
+ TopicNotification.objects.for_access(request.user),
+ per_page=config.topics_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+
+ context = {'notifications': notifications, }
+
+ return render(request, 'spirit/topic_notification/list.html', context)
diff --git a/spirit/views/topic_poll.py b/spirit/views/topic_poll.py
index fdc66928b..24c7a2a07 100644
--- a/spirit/views/topic_poll.py
+++ b/spirit/views/topic_poll.py
@@ -26,13 +26,18 @@ def poll_update(request, pk):
if form.is_valid() and formset.is_valid():
poll = form.save()
- choices = formset.save()
+ formset.save()
return redirect(request.POST.get('next', poll.get_absolute_url()))
else:
form = TopicPollForm(instance=poll)
formset = TopicPollChoiceFormSet(instance=poll)
- return render(request, 'spirit/topic_poll/poll_update.html', {'form': form, 'formset': formset})
+ context = {
+ 'form': form,
+ 'formset': formset
+ }
+
+ return render(request, 'spirit/topic_poll/poll_update.html', context)
@login_required
@@ -46,7 +51,9 @@ def poll_close(request, pk):
return redirect(request.GET.get('next', poll.get_absolute_url()))
- return render(request, 'spirit/topic_poll/poll_close.html', {'poll': poll, })
+ context = {'poll': poll, }
+
+ return render(request, 'spirit/topic_poll/poll_close.html', context)
@require_POST
@@ -65,6 +72,6 @@ def poll_vote(request, pk):
form.save_m2m()
topic_poll_post_vote.send(sender=poll.__class__, poll=poll, user=request.user)
return redirect(request.POST.get('next', poll.get_absolute_url()))
- else:
- messages.error(request, utils.render_form_errors(form))
- return redirect(request.POST.get('next', poll.get_absolute_url()))
+
+ messages.error(request, utils.render_form_errors(form))
+ return redirect(request.POST.get('next', poll.get_absolute_url()))
diff --git a/spirit/views/topic_private.py b/spirit/views/topic_private.py
index ad8535bb7..ef2133434 100644
--- a/spirit/views/topic_private.py
+++ b/spirit/views/topic_private.py
@@ -11,17 +11,21 @@
from django.http import HttpResponsePermanentRedirect
from django.conf import settings
-from spirit.utils.ratelimit.decorators import ratelimit
-from spirit import utils
-from spirit.forms.comment import CommentForm
-from spirit.signals.comment import comment_posted
+from djconfig import config
-from spirit.models.topic import Topic
-from spirit.signals.topic import topic_viewed
+from .. import utils
+from ..utils.paginator import paginate, yt_paginate
+from ..utils.ratelimit.decorators import ratelimit
+from ..forms.comment import CommentForm
+from ..signals.comment import comment_posted
+
+from ..models.comment import Comment
+from ..models.topic import Topic
+from ..signals.topic import topic_viewed
from ..models.topic_private import TopicPrivate
from ..forms.topic_private import TopicPrivateManyForm, TopicForPrivateForm,\
TopicPrivateJoinForm, TopicPrivateInviteForm
-from spirit.signals.topic_private import topic_private_post_create, topic_private_access_pre_create
+from ..signals.topic_private import topic_private_post_create, topic_private_access_pre_create
User = get_user_model()
@@ -57,26 +61,45 @@ def private_publish(request, user_id=None):
tpform = TopicPrivateManyForm(initial=initial)
- return render(request, 'spirit/topic_private/private_publish.html', {'tform': tform,
- 'cform': cform,
- 'tpform': tpform})
+ context = {
+ 'tform': tform,
+ 'cform': cform,
+ 'tpform': tpform
+ }
+
+ return render(request, 'spirit/topic_private/private_publish.html', context)
@login_required
def private_detail(request, topic_id, slug):
topic_private = get_object_or_404(TopicPrivate.objects.select_related('topic'),
- topic_id=topic_id, user=request.user)
+ topic_id=topic_id,
+ user=request.user)
+ topic = topic_private.topic
+
+ if topic.slug != slug:
+ return HttpResponsePermanentRedirect(topic.get_absolute_url())
+
+ topic_viewed.send(sender=topic.__class__, request=request, topic=topic)
- if topic_private.topic.slug != slug:
- return HttpResponsePermanentRedirect(topic_private.get_absolute_url())
+ comments = Comment.objects\
+ .for_topic(topic=topic)\
+ .with_likes(user=request.user)\
+ .order_by('date')
- topic_viewed.send(sender=topic_private.topic.__class__, request=request, topic=topic_private.topic)
+ comments = paginate(
+ comments,
+ per_page=config.comments_per_page,
+ page_number=request.GET.get('page', 1)
+ )
- return render(request,
- 'spirit/topic_private/private_detail.html',
- {'topic': topic_private.topic,
- 'topic_private': topic_private,
- 'COMMENTS_PER_PAGE': settings.ST_COMMENTS_PER_PAGE})
+ context = {
+ 'topic': topic,
+ 'topic_private': topic_private,
+ 'comments': comments,
+ }
+
+ return render(request, 'spirit/topic_private/private_detail.html', context)
@login_required
@@ -107,8 +130,10 @@ def private_access_delete(request, pk):
return redirect(reverse("spirit:private-list"))
return redirect(request.POST.get('next', topic_private.get_absolute_url()))
- else:
- return render(request, 'spirit/topic_private/private_delete.html', {'topic_private': topic_private, })
+
+ context = {'topic_private': topic_private, }
+
+ return render(request, 'spirit/topic_private/private_delete.html', context)
@login_required
@@ -127,19 +152,46 @@ def private_join(request, topic_id):
else:
form = TopicPrivateJoinForm()
- return render(request, 'spirit/topic_private/private_join.html', {'topic': topic, 'form': form, })
+ context = {
+ 'topic': topic,
+ 'form': form
+ }
+
+ return render(request, 'spirit/topic_private/private_join.html', context)
@login_required
def private_list(request):
- topics = Topic.objects.filter(topics_private__user=request.user).order_by('-last_active')
- return render(request, 'spirit/topic_private/private_list.html', {'topics': topics, })
+ topics = Topic.objects\
+ .with_bookmarks(user=request.user)\
+ .filter(topics_private__user=request.user)
+
+ topics = yt_paginate(
+ topics,
+ per_page=config.topics_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+
+ context = {'topics': topics, }
+
+ return render(request, 'spirit/topic_private/private_list.html', context)
@login_required
def private_created_list(request):
# Show created topics but exclude those the user is participating on
# TODO: show all, show join link in those the user is not participating
- topics = Topic.objects.filter(user=request.user, category_id=settings.ST_TOPIC_PRIVATE_CATEGORY_PK)\
+ # TODO: move to manager
+ topics = Topic.objects\
+ .filter(user=request.user, category_id=settings.ST_TOPIC_PRIVATE_CATEGORY_PK)\
.exclude(topics_private__user=request.user)
- return render(request, 'spirit/topic_private/private_created_list.html', {'topics': topics, })
+
+ topics = yt_paginate(
+ topics,
+ per_page=config.topics_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+
+ context = {'topics': topics, }
+
+ return render(request, 'spirit/topic_private/private_created_list.html', context)
diff --git a/spirit/views/topic_unread.py b/spirit/views/topic_unread.py
index ae250c5b0..be82d22cb 100644
--- a/spirit/views/topic_unread.py
+++ b/spirit/views/topic_unread.py
@@ -15,9 +15,10 @@ def topic_unread_list(request):
# TODO: add button to clean up read topics? or read all?
# redirect to first page if empty
- topics = Topic.objects.for_access(request.user)\
- .filter(topicunread__user=request.user,
- topicunread__is_read=False)
+ topics = Topic.objects\
+ .for_access(user=request.user)\
+ .for_unread(user=request.user)\
+ .with_bookmarks(user=request.user)
page = paginate(request, query_set=topics, lookup_field="last_active", page_var='topic_id')
next_page_pk = None
@@ -25,5 +26,9 @@ def topic_unread_list(request):
if page:
next_page_pk = page[-1].pk
- return render(request, 'spirit/topic_unread/list.html', {'page': page,
- 'next_page_pk': next_page_pk})
+ context = {
+ 'page': page,
+ 'next_page_pk': next_page_pk
+ }
+
+ return render(request, 'spirit/topic_unread/list.html', context)
diff --git a/spirit/views/user.py b/spirit/views/user.py
index 449db9fec..600a2905f 100644
--- a/spirit/views/user.py
+++ b/spirit/views/user.py
@@ -13,9 +13,12 @@
from django.utils.translation import ugettext as _
from django.http import HttpResponsePermanentRedirect
-from spirit.utils.ratelimit.decorators import ratelimit
-from spirit.utils.user.email import send_activation_email, send_email_change_email
-from spirit.utils.user.tokens import UserActivationTokenGenerator, UserEmailChangeTokenGenerator
+from djconfig import config
+
+from ..utils.ratelimit.decorators import ratelimit
+from ..utils.user.email import send_activation_email, send_email_change_email
+from ..utils.user.tokens import UserActivationTokenGenerator, UserEmailChangeTokenGenerator
+from ..utils.paginator import yt_paginate
from ..models.topic import Topic
from ..models.comment import Comment
@@ -27,6 +30,7 @@
@ratelimit(field='username', rate='5/5m')
+# TODO: @guest_only
def custom_login(request, **kwargs):
# Current Django 1.5 login view does not redirect somewhere if the user is logged in
if request.user.is_authenticated():
@@ -38,6 +42,7 @@ def custom_login(request, **kwargs):
return login_view(request, authentication_form=LoginForm, **kwargs)
+# TODO: @login_required ?
def custom_logout(request, **kwargs):
# Current Django 1.6 uses GET to log out
if not request.user.is_authenticated():
@@ -58,6 +63,7 @@ def custom_reset_password(request, **kwargs):
@ratelimit(rate='2/10s')
+# TODO: @guest_only
def register(request):
if request.user.is_authenticated():
return redirect(request.GET.get('next', reverse('spirit:profile-update')))
@@ -79,7 +85,9 @@ def register(request):
else:
form = RegistrationForm()
- return render(request, 'spirit/user/register.html', {'form': form, })
+ context = {'form': form, }
+
+ return render(request, 'spirit/user/register.html', context)
def registration_activation(request, pk, token):
@@ -87,6 +95,7 @@ def registration_activation(request, pk, token):
activation = UserActivationTokenGenerator()
if activation.is_valid(user, token):
+ user.is_verified = True
user.is_active = True
user.save()
messages.info(request, _("Your account has been activated!"))
@@ -95,6 +104,7 @@ def registration_activation(request, pk, token):
@ratelimit(field='email', rate='5/5m')
+# TODO: @guest_only
def resend_activation_email(request):
if request.user.is_authenticated():
return redirect(request.GET.get('next', reverse('spirit:profile-update')))
@@ -106,13 +116,16 @@ def resend_activation_email(request):
user = form.get_user()
send_activation_email(request, user)
+ # TODO: show if is_valid only
messages.info(request, _("If you don't receive an email, please make sure you've entered "
"the address you registered with, and check your spam folder."))
return redirect(reverse('spirit:user-login'))
else:
form = ResendActivationForm()
- return render(request, 'spirit/user/activation_resend.html', {'form': form, })
+ context = {'form': form, }
+
+ return render(request, 'spirit/user/activation_resend.html', context)
@login_required
@@ -127,7 +140,9 @@ def profile_update(request):
else:
form = UserProfileForm(instance=request.user)
- return render(request, 'spirit/user/profile_update.html', {'form': form, })
+ context = {'form': form, }
+
+ return render(request, 'spirit/user/profile_update.html', context)
@login_required
@@ -142,7 +157,9 @@ def profile_password_change(request):
else:
form = PasswordChangeForm(user=request.user)
- return render(request, 'spirit/user/profile_password_change.html', {'form': form, })
+ context = {'form': form, }
+
+ return render(request, 'spirit/user/profile_password_change.html', context)
@login_required
@@ -157,7 +174,9 @@ def profile_email_change(request):
else:
form = EmailChangeForm()
- return render(request, 'spirit/user/profile_email_change.html', {'form': form, })
+ context = {'form': form, }
+
+ return render(request, 'spirit/user/profile_email_change.html', context)
@login_required
@@ -178,12 +197,28 @@ def profile_topics(request, pk, slug):
p_user = get_object_or_404(User, pk=pk)
if p_user.slug != slug:
- return HttpResponsePermanentRedirect(reverse("spirit:profile-topics", kwargs={'pk': p_user.pk,
- 'slug': p_user.slug}))
+ url = reverse("spirit:profile-topics", kwargs={'pk': p_user.pk, 'slug': p_user.slug})
+ return HttpResponsePermanentRedirect(url)
- topics = Topic.objects.for_public().filter(user=p_user).order_by('-date').select_related('user')
+ topics = Topic.objects\
+ .visible()\
+ .with_bookmarks(user=request.user)\
+ .filter(user=p_user)\
+ .order_by('-date', '-pk')\
+ .select_related('user')
- return render(request, 'spirit/user/profile_topics.html', {'p_user': p_user, 'topics': topics})
+ topics = yt_paginate(
+ topics,
+ per_page=config.topics_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+
+ context = {
+ 'p_user': p_user,
+ 'topics': topics
+ }
+
+ return render(request, 'spirit/user/profile_topics.html', context)
@login_required
@@ -191,11 +226,25 @@ def profile_comments(request, pk, slug):
p_user = get_object_or_404(User, pk=pk)
if p_user.slug != slug:
- return HttpResponsePermanentRedirect(reverse("spirit:profile-detail", kwargs={'pk': p_user.pk,
- 'slug': p_user.slug}))
+ url = reverse("spirit:profile-detail", kwargs={'pk': p_user.pk, 'slug': p_user.slug})
+ return HttpResponsePermanentRedirect(url)
+
+ comments = Comment.objects\
+ .visible()\
+ .filter(user=p_user)
- comments = Comment.objects.for_user_public(user=p_user)
- return render(request, 'spirit/user/profile_comments.html', {'p_user': p_user, 'comments': comments})
+ comments = yt_paginate(
+ comments,
+ per_page=config.comments_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+
+ context = {
+ 'p_user': p_user,
+ 'comments': comments
+ }
+
+ return render(request, 'spirit/user/profile_comments.html', context)
@login_required
@@ -203,11 +252,26 @@ def profile_likes(request, pk, slug):
p_user = get_object_or_404(User, pk=pk)
if p_user.slug != slug:
- return HttpResponsePermanentRedirect(reverse("spirit:profile-likes", kwargs={'pk': p_user.pk,
- 'slug': p_user.slug}))
-
- comments = Comment.objects.for_public().filter(comment_likes__user=p_user).order_by('-comment_likes__date')
- return render(request, 'spirit/user/profile_likes.html', {'p_user': p_user, 'comments': comments})
+ url = reverse("spirit:profile-likes", kwargs={'pk': p_user.pk, 'slug': p_user.slug})
+ return HttpResponsePermanentRedirect(url)
+
+ comments = Comment.objects\
+ .visible()\
+ .filter(comment_likes__user=p_user)\
+ .order_by('-comment_likes__date', '-pk')
+
+ comments = yt_paginate(
+ comments,
+ per_page=config.comments_per_page,
+ page_number=request.GET.get('page', 1)
+ )
+
+ context = {
+ 'p_user': p_user,
+ 'comments': comments
+ }
+
+ return render(request, 'spirit/user/profile_likes.html', context)
@login_required
diff --git a/spirit/tests/migrations/__init__.py b/tests/__init__.py
similarity index 100%
rename from spirit/tests/migrations/__init__.py
rename to tests/__init__.py
diff --git a/spirit/tests/migrations/0001_initial.py b/tests/migrations/0001_initial.py
similarity index 100%
rename from spirit/tests/migrations/0001_initial.py
rename to tests/migrations/0001_initial.py
diff --git a/tests/migrations/__init__.py b/tests/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/spirit/tests/models/__init__.py b/tests/models/__init__.py
similarity index 100%
rename from spirit/tests/models/__init__.py
rename to tests/models/__init__.py
diff --git a/spirit/tests/models/auto_slug.py b/tests/models/auto_slug.py
similarity index 100%
rename from spirit/tests/models/auto_slug.py
rename to tests/models/auto_slug.py
diff --git a/example/local_settings_sample_dev.py b/tests/settings.py
similarity index 52%
rename from example/local_settings_sample_dev.py
rename to tests/settings.py
index ffc606c71..8365b4b60 100644
--- a/example/local_settings_sample_dev.py
+++ b/tests/settings.py
@@ -1,13 +1,26 @@
# -*- coding: utf-8 -*-
-
-# THIS IS FOR DEVELOPMENT ENVIRONMENT, MOSTLY TO SPEED UP TESTS
-# DO NOT USE IT IN PRODUCTION
+"""
+Django settings for running the tests of spirit app
+"""
from __future__ import unicode_literals
import os
-import sys
+from spirit.settings import *
+
+
+SECRET_KEY = 'TEST'
+
+INSTALLED_APPS += (
+ 'tests',
+)
+
+ROOT_URLCONF = 'tests.urls'
+
+USE_TZ = True
+
+STATIC_URL = '/static/'
DEBUG = True
@@ -20,37 +33,27 @@
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ 'NAME': os.path.join(BASE_DIR, 'db_test.sqlite3'),
}
}
-CACHES = {
+CACHES.update({
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
- 'djconfig': {
- 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
- },
-}
+})
+# speedup tests requiring login
PASSWORD_HASHERS = (
'django.contrib.auth.hashers.MD5PasswordHasher',
)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
-TESTING = 'test' in sys.argv
-
-if TESTING:
- # Keep templates in memory
- TEMPLATE_LOADERS = (
- ('django.template.loaders.cached.Loader', (
- 'django.template.loaders.filesystem.Loader',
- 'django.template.loaders.app_directories.Loader',
- )),
- )
-else:
- TEMPLATE_LOADERS = (
+# Keep templates in memory to speedup tests
+TEMPLATE_LOADERS = (
+ ('django.template.loaders.cached.Loader', (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
- )
+ )),
+)
diff --git a/spirit/tests/tests_admin.py b/tests/tests_admin.py
similarity index 70%
rename from spirit/tests/tests_admin.py
rename to tests/tests_admin.py
index 6f9882239..81ce58141 100644
--- a/spirit/tests/tests_admin.py
+++ b/tests/tests_admin.py
@@ -9,6 +9,8 @@
from django.contrib.auth.models import User as UserModel
from django.contrib.auth import get_user_model
+from djconfig.utils import override_djconfig
+
from . import utils
from spirit.views.admin import user, category, comment_flag, config, index, topic
@@ -78,6 +80,17 @@ def test_user_list(self):
response = self.client.get(reverse('spirit:admin-user-list'))
self.assertQuerysetEqual(response.context['users'], map(repr, [self.user, ]))
+ @override_djconfig(topics_per_page=1)
+ def test_user_list_paginate(self):
+ """
+ List of all users paginated
+ """
+ user2 = utils.create_user()
+
+ utils.login(self)
+ response = self.client.get(reverse('spirit:admin-user-list'))
+ self.assertQuerysetEqual(response.context['users'], map(repr, [user2, ]))
+
def test_user_admins(self):
"""
List of admins
@@ -86,11 +99,34 @@ def test_user_admins(self):
response = self.client.get(reverse('spirit:admin-user-admins'))
self.assertQuerysetEqual(response.context['users'], map(repr, [self.user, ]))
+ @override_djconfig(topics_per_page=1)
+ def test_user_admins_paginate(self):
+ """
+ List of admins paginated
+ """
+ user2 = utils.create_user(is_administrator=True)
+
+ utils.login(self)
+ response = self.client.get(reverse('spirit:admin-user-admins'))
+ self.assertQuerysetEqual(response.context['users'], map(repr, [user2, ]))
+
def test_user_mods(self):
"""
- List of admins
+ List of mods
+ """
+ mod = utils.create_user(is_moderator=True)
+ utils.login(self)
+ response = self.client.get(reverse('spirit:admin-user-mods'))
+ self.assertQuerysetEqual(response.context['users'], map(repr, [mod, ]))
+
+ @override_djconfig(topics_per_page=1)
+ def test_user_mods_paginate(self):
+ """
+ List of mods paginated
"""
+ utils.create_user(is_moderator=True)
mod = utils.create_user(is_moderator=True)
+
utils.login(self)
response = self.client.get(reverse('spirit:admin-user-mods'))
self.assertQuerysetEqual(response.context['users'], map(repr, [mod, ]))
@@ -105,6 +141,20 @@ def test_user_unactive(self):
response = self.client.get(reverse('spirit:admin-user-unactive'))
self.assertQuerysetEqual(response.context['users'], map(repr, [unactive, ]))
+ @override_djconfig(topics_per_page=1)
+ def test_user_unactive_paginate(self):
+ """
+ List of unactive paginated
+ """
+ unactive = utils.create_user()
+ User.objects.filter(pk=unactive.pk).update(is_active=False)
+ unactive2 = utils.create_user()
+ User.objects.filter(pk=unactive2.pk).update(is_active=False)
+
+ utils.login(self)
+ response = self.client.get(reverse('spirit:admin-user-unactive'))
+ self.assertQuerysetEqual(response.context['users'], map(repr, [unactive2, ]))
+
def test_index_dashboard(self):
utils.login(self)
response = self.client.get(reverse('spirit:admin-topic'))
@@ -119,6 +169,18 @@ def test_topic_deleted(self):
response = self.client.get(reverse('spirit:admin-topic-deleted'))
self.assertQuerysetEqual(response.context['topics'], map(repr, [topic_, ]))
+ @override_djconfig(topics_per_page=1)
+ def test_topic_deleted_paginate(self):
+ """
+ Deleted topics paginated
+ """
+ utils.create_topic(self.category, is_removed=True)
+ topic_ = utils.create_topic(self.category, is_removed=True)
+
+ utils.login(self)
+ response = self.client.get(reverse('spirit:admin-topic-deleted'))
+ self.assertQuerysetEqual(response.context['topics'], map(repr, [topic_, ]))
+
def test_topic_closed(self):
"""
Closed topics
@@ -128,6 +190,17 @@ def test_topic_closed(self):
response = self.client.get(reverse('spirit:admin-topic-closed'))
self.assertQuerysetEqual(response.context['topics'], map(repr, [topic_, ]))
+ @override_djconfig(topics_per_page=1)
+ def test_topic_closed_paginate(self):
+ """
+ Closed topics paginated
+ """
+ utils.create_topic(self.category, is_closed=True)
+ topic_ = utils.create_topic(self.category, is_closed=True)
+ utils.login(self)
+ response = self.client.get(reverse('spirit:admin-topic-closed'))
+ self.assertQuerysetEqual(response.context['topics'], map(repr, [topic_, ]))
+
def test_topic_pinned(self):
"""
Pinned topics
@@ -137,11 +210,22 @@ def test_topic_pinned(self):
response = self.client.get(reverse('spirit:admin-topic-pinned'))
self.assertQuerysetEqual(response.context['topics'], map(repr, [topic_, ]))
+ @override_djconfig(topics_per_page=1)
+ def test_topic_pinned_paginate(self):
+ """
+ Pinned topics paginated
+ """
+ utils.create_topic(self.category, is_pinned=True)
+ topic_ = utils.create_topic(self.category, is_pinned=True)
+ utils.login(self)
+ response = self.client.get(reverse('spirit:admin-topic-pinned'))
+ self.assertQuerysetEqual(response.context['topics'], map(repr, [topic_, ]))
+
def test_category_list(self):
"""
Categories, excludes Topic Private and subcats
"""
- subcat = utils.create_category(parent=self.category)
+ utils.create_category(parent=self.category)
categories = Category.objects.filter(is_private=False, parent=None)
utils.login(self)
response = self.client.get(reverse('spirit:admin-category-list'))
@@ -180,7 +264,7 @@ def test_config_basic(self):
Config
"""
utils.login(self)
- form_data = {"site_name": "foo", "site_description": "bar"}
+ form_data = {"site_name": "foo", "site_description": "bar", "comments_per_page": 10, "topics_per_page": 10}
response = self.client.post(reverse('spirit:admin-config-basic'),
form_data)
expected_url = reverse('spirit:admin-config-basic')
@@ -195,7 +279,21 @@ def test_flag_open(self):
"""
comment = utils.create_comment(topic=self.topic)
comment2 = utils.create_comment(topic=self.topic)
- flag_closed = CommentFlag.objects.create(comment=comment2, is_closed=True)
+ CommentFlag.objects.create(comment=comment2, is_closed=True)
+ flag_ = CommentFlag.objects.create(comment=comment)
+
+ utils.login(self)
+ response = self.client.get(reverse('spirit:admin-flag-open'))
+ self.assertQuerysetEqual(response.context['flags'], map(repr, [flag_, ]))
+
+ @override_djconfig(comments_per_page=1)
+ def test_flag_open_paginate(self):
+ """
+ Open flags paginated
+ """
+ comment = utils.create_comment(topic=self.topic)
+ comment2 = utils.create_comment(topic=self.topic)
+ CommentFlag.objects.create(comment=comment2)
flag_ = CommentFlag.objects.create(comment=comment)
utils.login(self)
@@ -209,7 +307,21 @@ def test_flag_closed(self):
comment = utils.create_comment(topic=self.topic)
comment2 = utils.create_comment(topic=self.topic)
flag_closed = CommentFlag.objects.create(comment=comment2, is_closed=True)
- flag_ = CommentFlag.objects.create(comment=comment)
+ CommentFlag.objects.create(comment=comment)
+
+ utils.login(self)
+ response = self.client.get(reverse('spirit:admin-flag-closed'))
+ self.assertQuerysetEqual(response.context['flags'], map(repr, [flag_closed, ]))
+
+ @override_djconfig(comments_per_page=1)
+ def test_flag_open_paginate(self):
+ """
+ Open flags paginated
+ """
+ comment = utils.create_comment(topic=self.topic)
+ comment2 = utils.create_comment(topic=self.topic)
+ CommentFlag.objects.create(comment=comment2, is_closed=True)
+ flag_closed = CommentFlag.objects.create(comment=comment, is_closed=True)
utils.login(self)
response = self.client.get(reverse('spirit:admin-flag-closed'))
@@ -224,8 +336,8 @@ def test_flag_detail(self):
flag_ = Flag.objects.create(comment=comment, user=self.user, reason=0)
comment2 = utils.create_comment(topic=self.topic)
- comment_flag2 = CommentFlag.objects.create(comment=comment2)
- flag_2 = Flag.objects.create(comment=comment2, user=self.user, reason=0)
+ CommentFlag.objects.create(comment=comment2)
+ Flag.objects.create(comment=comment2, user=self.user, reason=0)
utils.login(self)
form_data = {"is_closed": True, }
@@ -239,6 +351,22 @@ def test_flag_detail(self):
self.assertEqual(repr(response.context['flag']), repr(comment_flag))
self.assertQuerysetEqual(response.context['flags'], map(repr, [flag_, ]))
+ @override_djconfig(comments_per_page=1)
+ def test_flag_detail_paginate(self):
+ """
+ flag detail paginated
+ """
+ user2 = utils.create_user()
+ comment = utils.create_comment(topic=self.topic)
+ comment_flag = CommentFlag.objects.create(comment=comment)
+ Flag.objects.create(comment=comment, user=user2, reason=0)
+ flag_ = Flag.objects.create(comment=comment, user=self.user, reason=0)
+
+ utils.login(self)
+ response = self.client.get(reverse('spirit:admin-flag-detail', kwargs={'pk': comment_flag.pk, }))
+ self.assertEqual(response.status_code, 200)
+ self.assertQuerysetEqual(response.context['flags'], map(repr, [flag_, ]))
+
class AdminFormTest(TestCase):
@@ -312,13 +440,17 @@ def test_basic_config(self):
"""
form_data = {"site_name": "foo",
"site_description": "",
- "template_footer": ""}
+ "template_footer": "",
+ "comments_per_page": 10,
+ "topics_per_page": 10}
form = BasicConfigForm(data=form_data)
self.assertEqual(form.is_valid(), True)
form_data = {"site_name": "foo",
"site_description": "bar",
- "template_footer": "foobar"}
+ "template_footer": "foobar",
+ "comments_per_page": 10,
+ "topics_per_page": 10}
form = BasicConfigForm(data=form_data)
self.assertEqual(form.is_valid(), True)
diff --git a/spirit/tests/tests_category.py b/tests/tests_category.py
similarity index 62%
rename from spirit/tests/tests_category.py
rename to tests/tests_category.py
index 780a9ac4b..bbc5c2040 100644
--- a/spirit/tests/tests_category.py
+++ b/tests/tests_category.py
@@ -9,15 +9,19 @@
from django.core.cache import cache
from django.utils import timezone
+from djconfig.utils import override_djconfig
+
from . import utils
from spirit.models.topic import Topic
+from spirit.models.comment_bookmark import CommentBookmark
class CategoryViewTest(TestCase):
def setUp(self):
cache.clear()
+ self.user = utils.create_user()
self.category_1 = utils.create_category(title="cat1")
self.subcategory_1 = utils.create_subcategory(self.category_1)
self.category_2 = utils.create_category(title="cat2")
@@ -28,8 +32,10 @@ def test_category_list_view(self):
should display all categories
"""
response = self.client.get(reverse('spirit:category-list'))
- self.assertQuerysetEqual(response.context['categories'],
- ['', repr(self.category_1), repr(self.category_2)])
+ self.assertQuerysetEqual(
+ response.context['categories'],
+ ['', repr(self.category_1), repr(self.category_2)]
+ )
def test_category_detail_view(self):
"""
@@ -53,7 +59,7 @@ def test_category_detail_view_order(self):
"""
topic_a = utils.create_topic(category=self.category_1, is_pinned=True)
topic_b = utils.create_topic(category=self.category_1)
- topic_c = utils.create_topic(category=self.category_1, is_pinned=True, is_removed=True)
+ utils.create_topic(category=self.category_1, is_pinned=True, is_removed=True)
# show pinned first
Topic.objects.filter(pk=topic_a.pk).update(last_active=timezone.now() - datetime.timedelta(days=10))
@@ -61,14 +67,30 @@ def test_category_detail_view_order(self):
'slug': self.category_1.slug}))
self.assertQuerysetEqual(response.context['topics'], map(repr, [topic_a, topic_b, ]))
+ def test_category_detail_view_pinned(self):
+ """
+ Show globally pinned topics first, then regular pinned topics, then regular topics
+ """
+ category = utils.create_category()
+ topic_a = utils.create_topic(category=category)
+ topic_b = utils.create_topic(category=category, is_pinned=True)
+ topic_c = utils.create_topic(category=category)
+ topic_d = utils.create_topic(category=category, is_globally_pinned=True)
+ # show globally pinned first
+ Topic.objects.filter(pk=topic_d.pk).update(last_active=timezone.now() - datetime.timedelta(days=10))
+
+ response = self.client.get(reverse('spirit:category-detail', kwargs={'pk': category.pk,
+ 'slug': category.slug}))
+ self.assertQuerysetEqual(response.context['topics'], map(repr, [topic_d, topic_b, topic_c, topic_a]))
+
def test_category_detail_view_removed_topics(self):
"""
should not display removed topics or from other categories
"""
subcategory_removed = utils.create_subcategory(self.category_1, is_removed=True)
- topic_removed = utils.create_topic(category=subcategory_removed)
- topic_removed2 = utils.create_topic(category=self.category_1, is_removed=True)
- topic_bad = utils.create_topic(category=self.category_2)
+ utils.create_topic(category=subcategory_removed)
+ utils.create_topic(category=self.category_1, is_removed=True)
+ utils.create_topic(category=self.category_2)
response = self.client.get(reverse('spirit:category-detail', kwargs={'pk': self.category_1.pk,
'slug': self.category_1.slug}))
@@ -101,9 +123,35 @@ def test_category_detail_subcategory(self):
"""
should display all topics in subcategory
"""
- topic = utils.create_topic(category=self.category_1)
+ utils.create_topic(category=self.category_1)
topic2 = utils.create_topic(category=self.subcategory_1, title="topic_subcat1")
response = self.client.get(reverse('spirit:category-detail', kwargs={'pk': self.subcategory_1.pk,
'slug': self.subcategory_1.slug}))
self.assertQuerysetEqual(response.context['topics'], [repr(topic2), ])
self.assertQuerysetEqual(response.context['categories'], [])
+
+ def test_category_detail_view_bookmarks(self):
+ """
+ topics should have bookmarks
+ """
+ utils.login(self)
+ topic = utils.create_topic(category=self.category_1)
+ bookmark = CommentBookmark.objects.create(topic=topic, user=self.user)
+
+ response = self.client.get(reverse('spirit:category-detail',
+ kwargs={'pk': self.category_1.pk,
+ 'slug': self.category_1.slug}))
+ self.assertQuerysetEqual(response.context['topics'], [repr(topic), ])
+ self.assertEqual(response.context['topics'][0].bookmark, bookmark)
+
+ @override_djconfig(topics_per_page=1)
+ def test_category_detail_view_paginate(self):
+ """
+ List of topics paginated
+ """
+ utils.create_topic(category=self.category_1)
+ topic = utils.create_topic(category=self.category_1)
+
+ response = self.client.get(reverse('spirit:category-detail', kwargs={'pk': self.category_1.pk,
+ 'slug': self.category_1.slug}))
+ self.assertQuerysetEqual(response.context['topics'], [repr(topic), ])
diff --git a/spirit/tests/tests_comment.py b/tests/tests_comment.py
similarity index 95%
rename from spirit/tests/tests_comment.py
rename to tests/tests_comment.py
index 192f5f3c4..bd070aaa4 100644
--- a/spirit/tests/tests_comment.py
+++ b/tests/tests_comment.py
@@ -19,9 +19,9 @@
from . import utils
-from spirit.models.comment import Comment,\
- comment_like_post_create, comment_like_post_delete,\
- topic_post_moderate
+from spirit.models.comment import Comment
+from spirit.signals.comment_like import comment_like_post_create, comment_like_post_delete
+from spirit.signals.topic_moderate import topic_post_moderate
from spirit.forms.comment import CommentForm, CommentMoveForm, CommentImageForm
from spirit.signals.comment import comment_post_update, comment_posted, comment_pre_update, comment_moved
from spirit.templatetags.tags.comment import render_comments_form
@@ -160,8 +160,7 @@ def comment_posted_handler(sender, comment, **kwargs):
utils.login(self)
form_data = {'comment': 'foobar', }
- response = self.client.post(reverse('spirit:comment-publish', kwargs={'topic_id': self.topic.pk, }),
- form_data)
+ self.client.post(reverse('spirit:comment-publish', kwargs={'topic_id': self.topic.pk, }), form_data)
self.assertEqual(self._comment.comment, 'foobar')
def test_comment_publish_quote(self):
@@ -234,6 +233,21 @@ def test_comment_update_moderator(self):
self.assertRedirects(response, expected_url, status_code=302, target_status_code=302)
self.assertEqual(Comment.objects.get(pk=comment.pk).comment, 'barfoo')
+ def test_comment_update_moderator_private(self):
+ """
+ moderators can not update comments in private topics they has no access
+ """
+ User.objects.filter(pk=self.user.pk).update(is_moderator=True)
+ user = utils.create_user()
+ topic_private = utils.create_private_topic()
+ comment = utils.create_comment(user=user, topic=topic_private.topic)
+
+ utils.login(self)
+ form_data = {'comment': 'barfoo', }
+ response = self.client.post(reverse('spirit:comment-update', kwargs={'pk': comment.pk, }),
+ form_data)
+ self.assertEqual(response.status_code, 404)
+
def test_comment_update_signal(self):
"""
update comment, emit signal
@@ -249,8 +263,8 @@ def comment_post_update_handler(sender, comment, **kwargs):
utils.login(self)
comment_posted = utils.create_comment(user=self.user, topic=self.topic)
form_data = {'comment': 'barfoo', }
- response = self.client.post(reverse('spirit:comment-update', kwargs={'pk': comment_posted.pk, }),
- form_data)
+ self.client.post(reverse('spirit:comment-update', kwargs={'pk': comment_posted.pk, }),
+ form_data)
self.assertEqual(repr(self._comment_new), repr(Comment.objects.get(pk=comment_posted.pk)))
self.assertEqual(repr(self._comment_old), repr(comment_posted))
@@ -441,24 +455,11 @@ def setUp(self):
utils.create_comment(topic=self.topic)
utils.create_comment(topic=self.topic)
- def test_get_comment_list(self):
- """
- should display all comment for a topic
- """
- out = Template(
- "{% load spirit_tags %}"
- "{% get_comment_list topic as comments %}"
- "{% for c in comments %}"
- "{{ c.comment }},"
- "{% endfor %}"
- ).render(Context({'topic': self.topic, }))
- self.assertEqual(out, "foobar0,foobar1,foobar2,")
-
def test_render_comments_form(self):
"""
should display simple comment form
"""
- out = Template(
+ Template(
"{% load spirit_tags %}"
"{% render_comments_form topic %}"
).render(Context({'topic': self.topic, }))
diff --git a/spirit/tests/tests_comment_bookmark.py b/tests/tests_comment_bookmark.py
similarity index 87%
rename from spirit/tests/tests_comment_bookmark.py
rename to tests/tests_comment_bookmark.py
index db85f61a3..4b02581b7 100644
--- a/spirit/tests/tests_comment_bookmark.py
+++ b/tests/tests_comment_bookmark.py
@@ -8,9 +8,12 @@
from django.core.cache import cache
from django.conf import settings
+from djconfig import config
+
from . import utils
-from spirit.models.comment_bookmark import CommentBookmark, topic_viewed
+from spirit.models.comment_bookmark import CommentBookmark
+from spirit.signals.topic import topic_viewed
from spirit.forms.comment_bookmark import BookmarkForm
@@ -43,7 +46,7 @@ def setUp(self):
self.category = utils.create_category()
self.topic = utils.create_topic(category=self.category, user=self.user)
- for _ in range(settings.ST_COMMENTS_PER_PAGE * 4): # 4 pages
+ for _ in range(config.comments_per_page * 4): # 4 pages
utils.create_comment(user=self.user, topic=self.topic)
def test_comment_bookmark_topic_page_viewed_handler(self):
@@ -51,18 +54,18 @@ def test_comment_bookmark_topic_page_viewed_handler(self):
topic_page_viewed_handler signal
"""
page = 2
- req = RequestFactory().get('/', data={settings.ST_COMMENTS_PAGE_VAR: str(page), })
+ req = RequestFactory().get('/', data={'page': str(page), })
req.user = self.user
topic_viewed.send(sender=self.topic.__class__, topic=self.topic, request=req)
comment_bookmark = CommentBookmark.objects.get(user=self.user, topic=self.topic)
- self.assertEqual(comment_bookmark.comment_number, settings.ST_COMMENTS_PER_PAGE * (page - 1) + 1)
+ self.assertEqual(comment_bookmark.comment_number, config.comments_per_page * (page - 1) + 1)
def test_comment_bookmark_topic_page_viewed_handler_invalid_page(self):
"""
invalid page
"""
page = 'im_a_string'
- req = RequestFactory().get('/', data={settings.ST_COMMENTS_PAGE_VAR: str(page), })
+ req = RequestFactory().get('/', data={'page': str(page), })
req.user = self.user
topic_viewed.send(sender=self.topic.__class__, topic=self.topic, request=req)
self.assertEqual(len(CommentBookmark.objects.all()), 0)
diff --git a/spirit/tests/tests_comment_flag.py b/tests/tests_comment_flag.py
similarity index 100%
rename from spirit/tests/tests_comment_flag.py
rename to tests/tests_comment_flag.py
diff --git a/spirit/tests/tests_comment_history.py b/tests/tests_comment_history.py
similarity index 78%
rename from spirit/tests/tests_comment_history.py
rename to tests/tests_comment_history.py
index 368c8a035..5b2169787 100644
--- a/spirit/tests/tests_comment_history.py
+++ b/tests/tests_comment_history.py
@@ -6,9 +6,12 @@
from django.core.urlresolvers import reverse
from django.core.cache import cache
+from djconfig.utils import override_djconfig
+
from . import utils
-from spirit.models.comment_history import CommentHistory, comment_pre_update, comment_post_update
+from spirit.models.comment_history import CommentHistory
+from spirit.signals.comment import comment_pre_update, comment_post_update
class CommentHistoryViewTest(TestCase):
@@ -26,7 +29,20 @@ def test_comment_history_detail(self):
comment = utils.create_comment(user=self.user, topic=self.topic)
comment_history = CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html)
comment2 = utils.create_comment(user=self.user, topic=self.topic)
- comment_history2 = CommentHistory.objects.create(comment_fk=comment2, comment_html=comment2.comment_html)
+ CommentHistory.objects.create(comment_fk=comment2, comment_html=comment2.comment_html)
+
+ utils.login(self)
+ response = self.client.get(reverse('spirit:comment-history', kwargs={'comment_id': comment.pk, }))
+ self.assertQuerysetEqual(response.context['comments'], map(repr, [comment_history, ]))
+
+ @override_djconfig(comments_per_page=1)
+ def test_comment_history_detail_paginate(self):
+ """
+ history comment paginate
+ """
+ comment = utils.create_comment(user=self.user, topic=self.topic)
+ comment_history = CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html)
+ CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html)
utils.login(self)
response = self.client.get(reverse('spirit:comment-history', kwargs={'comment_id': comment.pk, }))
@@ -38,7 +54,7 @@ def test_comment_history_detail_private_topic(self):
"""
private = utils.create_private_topic(user=self.user)
comment = utils.create_comment(user=self.user, topic=private.topic)
- comment_history = CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html)
+ CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html)
utils.login(self)
response = self.client.get(reverse('spirit:comment-history', kwargs={'comment_id': comment.pk, }))
@@ -52,14 +68,14 @@ def test_comment_history_detail_removed(self):
# comment removed
comment = utils.create_comment(user=self.user, topic=self.topic, is_removed=True)
- comment_history = CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html)
+ CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html)
response = self.client.get(reverse('spirit:comment-history', kwargs={'comment_id': comment.pk, }))
self.assertEqual(response.status_code, 404)
# topic removed
topic = utils.create_topic(category=self.category, user=self.user, is_removed=True)
comment = utils.create_comment(user=self.user, topic=topic)
- comment_history = CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html)
+ CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html)
response = self.client.get(reverse('spirit:comment-history', kwargs={'comment_id': comment.pk, }))
self.assertEqual(response.status_code, 404)
@@ -67,7 +83,7 @@ def test_comment_history_detail_removed(self):
category = utils.create_category(is_removed=True)
topic = utils.create_topic(category=category, user=self.user)
comment = utils.create_comment(user=self.user, topic=topic)
- comment_history = CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html)
+ CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html)
response = self.client.get(reverse('spirit:comment-history', kwargs={'comment_id': comment.pk, }))
self.assertEqual(response.status_code, 404)
@@ -79,7 +95,7 @@ def test_comment_history_detail_no_access(self):
private.delete()
comment = utils.create_comment(user=self.user, topic=private.topic)
- comment_history = CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html)
+ CommentHistory.objects.create(comment_fk=comment, comment_html=comment.comment_html)
utils.login(self)
response = self.client.get(reverse('spirit:comment-history', kwargs={'comment_id': comment.pk, }))
diff --git a/spirit/tests/tests_comment_like.py b/tests/tests_comment_like.py
similarity index 89%
rename from spirit/tests/tests_comment_like.py
rename to tests/tests_comment_like.py
index 964ed0e51..fd7e69150 100644
--- a/spirit/tests/tests_comment_like.py
+++ b/tests/tests_comment_like.py
@@ -9,6 +9,7 @@
from . import utils
+from spirit.models import Comment
from spirit.models.comment_like import CommentLike
from spirit.forms.comment_like import LikeForm
from spirit.templatetags.tags.comment_like import render_like_form
@@ -128,7 +129,7 @@ def test_like_render_like_form(self):
"{% render_like_form comment=comment like=like %}"
)
data = {'comment': self.comment, 'like': None}
- out = template.render(Context(data))
+ template.render(Context(data))
context = render_like_form(**data)
self.assertEqual(context['next'], None)
self.assertIsInstance(context['form'], LikeForm)
@@ -137,18 +138,6 @@ def test_like_render_like_form(self):
like = CommentLike.objects.create(user=self.user, comment=self.comment)
data['like'] = like
- out = template.render(Context(data))
+ template.render(Context(data))
context = render_like_form(**data)
self.assertEqual(context['like'], like)
-
- def test_like_populate_likes(self):
- """
- should populate comments likes, tell if current user liked the comment
- """
- like = CommentLike.objects.create(user=self.user, comment=self.comment)
- out = Template(
- "{% load spirit_tags %}"
- "{% populate_likes comments=comments user=user %}"
- "{{ comments.0.like }}"
- ).render(Context({'comments': [self.comment, ], 'user': self.user}))
- self.assertEqual(out, str(like))
diff --git a/spirit/tests/tests_gravatar.py b/tests/tests_gravatar.py
similarity index 81%
rename from spirit/tests/tests_gravatar.py
rename to tests/tests_gravatar.py
index 91125a19a..fe7a61395 100644
--- a/spirit/tests/tests_gravatar.py
+++ b/tests/tests_gravatar.py
@@ -20,4 +20,4 @@ def test_gravatar_url(self):
"{% load spirit_tags %}"
"{% get_gravatar_url user 21 %}"
).render(Context({'user': self.user, }))
- self.assertEqual(out, "http://www.gravatar.com/avatar/441cf33d0e5b36a95bae87e400783ca4?d=identicon&s=21&r=g")
+ self.assertEqual(out, "http://www.gravatar.com/avatar/472860d1aad501ba9795fb31e94ad42f?d=identicon&s=21&r=g")
diff --git a/spirit/tests/tests_search.py b/tests/tests_search.py
similarity index 86%
rename from spirit/tests/tests_search.py
rename to tests/tests_search.py
index dd921d0a5..4d84a1186 100644
--- a/spirit/tests/tests_search.py
+++ b/tests/tests_search.py
@@ -10,6 +10,7 @@
from django.core.management import call_command
from haystack.query import SearchQuerySet
+from djconfig.utils import override_djconfig
from . import utils
@@ -35,18 +36,18 @@ def test_index_queryset_excludes_private_topics(self):
"""
index_queryset should exclude private topics
"""
- private = utils.create_private_topic()
+ utils.create_private_topic()
self.assertEqual(len(TopicIndex().index_queryset()), 0)
category = utils.create_category()
- topic = utils.create_topic(category)
+ utils.create_topic(category)
self.assertEqual(len(TopicIndex().index_queryset()), 1)
def test_indexing_excludes_private_topics(self):
"""
rebuild_index command should exclude private topics
"""
- private = utils.create_private_topic()
+ utils.create_private_topic()
category = utils.create_category()
topic = utils.create_topic(category)
call_command("rebuild_index", interactive=False)
@@ -65,7 +66,7 @@ def setUp(self):
cache.clear()
self.user = utils.create_user()
self.category = utils.create_category()
- self.topic = utils.create_topic(category=self.category, user=self.user, title="spirit search test")
+ self.topic = utils.create_topic(category=self.category, user=self.user, title="spirit search test foo")
self.topic2 = utils.create_topic(category=self.category, user=self.user, title="foo")
call_command("rebuild_index", interactive=False)
@@ -92,6 +93,18 @@ def test_advanced_search_topics(self):
self.assertEqual(response.status_code, 200)
self.assertQuerysetEqual([s.object for s in response.context['page']], map(repr, [self.topic, ]))
+ @override_djconfig(topics_per_page=1)
+ def test_advanced_search_topics_paginate(self):
+ """
+ advanced search by topic paginated
+ """
+ utils.login(self)
+ data = {'q': 'foo', }
+ response = self.client.get(reverse('spirit:search'),
+ data)
+ self.assertEqual(response.status_code, 200)
+ self.assertQuerysetEqual([s.object for s in response.context['page']], map(repr, [self.topic2, ]))
+
def test_advanced_search_in_category(self):
"""
search by topic in category
@@ -144,7 +157,7 @@ def test_render_search_form(self):
"""
should display the basic search form
"""
- out = Template(
+ Template(
"{% load spirit_tags %}"
"{% render_search_form %}"
).render(Context())
diff --git a/spirit/tests/tests_topic.py b/tests/tests_topic.py
similarity index 70%
rename from spirit/tests/tests_topic.py
rename to tests/tests_topic.py
index 25a514d1b..1ebcd7262 100644
--- a/spirit/tests/tests_topic.py
+++ b/tests/tests_topic.py
@@ -8,17 +8,20 @@
from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.utils import timezone
-from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
+from djconfig.utils import override_djconfig
+
from . import utils
-from spirit.models.comment import MOVED, CLOSED, UNCLOSED, PINNED, UNPINNED
-from spirit.models.topic import Topic, topic_viewed, comment_posted, comment_moved
+from spirit.models.comment import MOVED
+from spirit.models.topic import Topic
+from spirit.signals.comment import comment_posted, comment_moved
+from spirit.signals.topic import topic_viewed
from spirit.forms.topic import TopicForm
-from spirit.signals.topic import topic_post_moderate
+from spirit.signals.topic_moderate import topic_post_moderate
from spirit.models.comment import Comment
-from spirit.models.category import Category
+from spirit.models.comment_bookmark import CommentBookmark
from spirit.forms.topic_poll import TopicPollForm, TopicPollChoiceFormSet
@@ -60,7 +63,7 @@ def test_topic_publish_long_title(self):
"""
utils.login(self)
category = utils.create_category()
- title = "a" * 75
+ title = "a" * 255
form_data = {'comment': 'foo', 'title': title, 'category': category.pk,
'choices-TOTAL_FORMS': 2, 'choices-INITIAL_FORMS': 0, 'choice_limit': 1}
response = self.client.post(reverse('spirit:topic-publish'),
@@ -182,8 +185,8 @@ def topic_post_moderate_handler(sender, user, topic, action, **kwargs):
topic = utils.create_topic(category=category, user=self.user)
category2 = utils.create_category()
form_data = {'title': 'foobar', 'category': category2.pk}
- response = self.client.post(reverse('spirit:topic-update', kwargs={'pk': topic.pk, }),
- form_data)
+ self.client.post(reverse('spirit:topic-update', kwargs={'pk': topic.pk, }),
+ form_data)
self.assertSequenceEqual(self._moderate, [repr(self.user), repr(Topic.objects.get(pk=topic.pk)), MOVED])
def test_topic_update_invalid_user(self):
@@ -200,14 +203,40 @@ def test_topic_update_invalid_user(self):
def test_topic_detail_view(self):
"""
- should display topic
+ should display topic with comments
"""
utils.login(self)
category = utils.create_category()
+
topic = utils.create_topic(category=category)
+ topic2 = utils.create_topic(category=category)
+
+ comment1 = utils.create_comment(topic=topic)
+ comment2 = utils.create_comment(topic=topic)
+ utils.create_comment(topic=topic2)
+
response = self.client.get(reverse('spirit:topic-detail', kwargs={'pk': topic.pk, 'slug': topic.slug}))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['topic'], topic)
+ self.assertQuerysetEqual(response.context['comments'], map(repr, [comment1, comment2]))
+
+ @override_djconfig(comments_per_page=2)
+ def test_topic_detail_view_paginate(self):
+ """
+ should display topic with comments, page 1
+ """
+ utils.login(self)
+ category = utils.create_category()
+
+ topic = utils.create_topic(category=category)
+
+ comment1 = utils.create_comment(topic=topic)
+ comment2 = utils.create_comment(topic=topic)
+ utils.create_comment(topic=topic) # comment3
+
+ response = self.client.get(reverse('spirit:topic-detail', kwargs={'pk': topic.pk, 'slug': topic.slug}))
+ self.assertEqual(response.status_code, 200)
+ self.assertQuerysetEqual(response.context['comments'], map(repr, [comment1, comment2]))
def test_topic_detail_view_signals(self):
"""
@@ -264,29 +293,18 @@ def test_topic_active_view(self):
def test_topic_active_view_pinned(self):
"""
- pinned topics. Only show pinned topics from uncategorized category, even if the category is removed
+ Show globally pinned topics first, regular pinned topics are shown as regular topics
"""
category = utils.create_category()
- # show topic from regular category
topic_a = utils.create_topic(category=category)
- # dont show pinned from regular category
topic_b = utils.create_topic(category=category, is_pinned=True)
-
- uncat_category = Category.objects.get(pk=settings.ST_UNCATEGORIZED_CATEGORY_PK)
- # dont show pinned and removed
- topic_c = utils.create_topic(category=uncat_category, is_pinned=True, is_removed=True)
- # show topic from uncategorized category
- topic_d = utils.create_topic(category=uncat_category, is_pinned=True)
- # show pinned first
+ topic_c = utils.create_topic(category=category)
+ topic_d = utils.create_topic(category=category, is_globally_pinned=True)
+ # show globally pinned first
Topic.objects.filter(pk=topic_d.pk).update(last_active=timezone.now() - datetime.timedelta(days=10))
response = self.client.get(reverse('spirit:topic-active'))
- self.assertQuerysetEqual(response.context['topics'], map(repr, [topic_d, topic_a, ]))
-
- # show topic from uncategorized category even if it is removed
- Category.objects.filter(pk=uncat_category.pk).update(is_removed=True)
- response = self.client.get(reverse('spirit:topic-active'))
- self.assertQuerysetEqual(response.context['topics'], map(repr, [topic_d, topic_a, ]))
+ self.assertQuerysetEqual(response.context['topics'], map(repr, [topic_d, topic_c, topic_b, topic_a]))
def test_topic_active_view_dont_show_private_or_removed(self):
"""
@@ -296,136 +314,47 @@ def test_topic_active_view_dont_show_private_or_removed(self):
category_removed = utils.create_category(is_removed=True)
subcategory = utils.create_category(parent=category_removed)
subcategory_removed = utils.create_category(parent=category, is_removed=True)
- topic_a = utils.create_private_topic()
- topic_b = utils.create_topic(category=category, is_removed=True)
- topic_c = utils.create_topic(category=category_removed)
- topic_d = utils.create_topic(category=subcategory)
- topic_e = utils.create_topic(category=subcategory_removed)
+ utils.create_private_topic()
+ utils.create_topic(category=category, is_removed=True)
+ utils.create_topic(category=category_removed)
+ utils.create_topic(category=subcategory)
+ utils.create_topic(category=subcategory_removed)
response = self.client.get(reverse('spirit:topic-active'))
self.assertQuerysetEqual(response.context['topics'], [])
- def test_topic_moderate_delete(self):
- """
- delete topic
- """
- utils.login(self)
- self.user.is_moderator = True
- self.user.save()
-
- category = utils.create_category()
- topic = utils.create_topic(category)
- form_data = {}
- response = self.client.post(reverse('spirit:topic-delete', kwargs={'pk': topic.pk, }),
- form_data)
- expected_url = topic.get_absolute_url()
- self.assertRedirects(response, expected_url, status_code=302)
- self.assertTrue(Topic.objects.get(pk=topic.pk).is_removed)
-
- def test_topic_moderate_undelete(self):
+ def test_topic_active_view_bookmark(self):
"""
- undelete topic
+ topics with bookmarks
"""
utils.login(self)
- self.user.is_moderator = True
- self.user.save()
-
category = utils.create_category()
- topic = utils.create_topic(category, is_removed=True)
- form_data = {}
- response = self.client.post(reverse('spirit:topic-undelete', kwargs={'pk': topic.pk, }),
- form_data)
- expected_url = topic.get_absolute_url()
- self.assertRedirects(response, expected_url, status_code=302)
- self.assertFalse(Topic.objects.get(pk=topic.pk).is_removed)
-
- def test_topic_moderate_lock(self):
- """
- topic lock
- """
- def topic_post_moderate_handler(sender, user, topic, action, **kwargs):
- self._moderate = [repr(user._wrapped), repr(topic), action]
- topic_post_moderate.connect(topic_post_moderate_handler)
-
- utils.login(self)
- self.user.is_moderator = True
- self.user.save()
-
- category = utils.create_category()
- topic = utils.create_topic(category)
- form_data = {}
- response = self.client.post(reverse('spirit:topic-lock', kwargs={'pk': topic.pk, }),
- form_data)
- expected_url = topic.get_absolute_url()
- self.assertRedirects(response, expected_url, status_code=302)
- self.assertTrue(Topic.objects.get(pk=topic.pk).is_closed)
- self.assertEqual(self._moderate, [repr(self.user), repr(topic), CLOSED])
+ topic = utils.create_topic(category=category, user=self.user)
+ bookmark = CommentBookmark.objects.create(topic=topic, user=self.user)
- def test_topic_moderate_unlock(self):
- """
- unlock topic
- """
- def topic_post_moderate_handler(sender, user, topic, action, **kwargs):
- self._moderate = [repr(user._wrapped), repr(topic), action]
- topic_post_moderate.connect(topic_post_moderate_handler)
+ user2 = utils.create_user()
+ CommentBookmark.objects.create(topic=topic, user=user2)
- utils.login(self)
- self.user.is_moderator = True
- self.user.save()
+ topic2 = utils.create_topic(category=category, user=self.user)
+ CommentBookmark.objects.create(topic=topic2, user=self.user)
+ ten_days_ago = timezone.now() - datetime.timedelta(days=10)
+ Topic.objects.filter(pk=topic2.pk).update(last_active=ten_days_ago)
- category = utils.create_category()
- topic = utils.create_topic(category, is_closed=True)
- form_data = {}
- response = self.client.post(reverse('spirit:topic-unlock', kwargs={'pk': topic.pk, }),
- form_data)
- expected_url = topic.get_absolute_url()
- self.assertRedirects(response, expected_url, status_code=302)
- self.assertFalse(Topic.objects.get(pk=topic.pk).is_closed)
- self.assertEqual(self._moderate, [repr(self.user), repr(topic), UNCLOSED])
+ response = self.client.get(reverse('spirit:topic-active'))
+ self.assertQuerysetEqual(response.context['topics'], map(repr, [topic, topic2]))
+ self.assertEqual(response.context['topics'][0].bookmark, bookmark)
- def test_topic_moderate_pin(self):
+ @override_djconfig(topics_per_page=1)
+ def test_topic_active_view_paginate(self):
"""
- topic pin
+ topics ordered by activity paginated
"""
- def topic_post_moderate_handler(sender, user, topic, action, **kwargs):
- self._moderate = [repr(user._wrapped), repr(topic), action]
- topic_post_moderate.connect(topic_post_moderate_handler)
-
- utils.login(self)
- self.user.is_moderator = True
- self.user.save()
-
category = utils.create_category()
- topic = utils.create_topic(category)
- form_data = {}
- response = self.client.post(reverse('spirit:topic-pin', kwargs={'pk': topic.pk, }),
- form_data)
- expected_url = topic.get_absolute_url()
- self.assertRedirects(response, expected_url, status_code=302)
- self.assertTrue(Topic.objects.get(pk=topic.pk).is_pinned)
- self.assertEqual(self._moderate, [repr(self.user), repr(topic), PINNED])
-
- def test_topic_moderate_unpin(self):
- """
- topic unpin
- """
- def topic_post_moderate_handler(sender, user, topic, action, **kwargs):
- self._moderate = [repr(user._wrapped), repr(topic), action]
- topic_post_moderate.connect(topic_post_moderate_handler)
-
- utils.login(self)
- self.user.is_moderator = True
- self.user.save()
+ topic_a = utils.create_topic(category=category)
+ topic_b = utils.create_topic(category=category, user=self.user, view_count=10)
- category = utils.create_category()
- topic = utils.create_topic(category, is_pinned=True)
- form_data = {}
- response = self.client.post(reverse('spirit:topic-unpin', kwargs={'pk': topic.pk, }),
- form_data)
- expected_url = topic.get_absolute_url()
- self.assertRedirects(response, expected_url, status_code=302)
- self.assertFalse(Topic.objects.get(pk=topic.pk).is_pinned)
- self.assertEqual(self._moderate, [repr(self.user), repr(topic), UNPINNED])
+ response = self.client.get(reverse('spirit:topic-active'))
+ self.assertQuerysetEqual(response.context['topics'], map(repr, [topic_b, ]))
class TopicFormTest(TestCase):
@@ -445,9 +374,9 @@ def test_topic_publish(self):
form = TopicForm(self.user, data=form_data)
self.assertEqual(form.is_valid(), True)
- def test_topic_publish_invalid_subcategory(self):
+ def test_topic_publish_invalid_closed_subcategory(self):
"""
- invalid subcategory
+ invalid closed subcategory
"""
category = utils.create_category()
subcategory = utils.create_subcategory(category, is_closed=True)
@@ -457,6 +386,18 @@ def test_topic_publish_invalid_subcategory(self):
self.assertEqual(form.is_valid(), False)
self.assertNotIn('category', form.cleaned_data)
+ def test_topic_publish_invalid_removed_subcategory(self):
+ """
+ invalid removed subcategory
+ """
+ category = utils.create_category()
+ subcategory = utils.create_subcategory(category, is_removed=True)
+ form_data = {'comment': 'foo', 'title': 'foobar',
+ 'category': subcategory.pk}
+ form = TopicForm(self.user, data=form_data)
+ self.assertEqual(form.is_valid(), False)
+ self.assertNotIn('category', form.cleaned_data)
+
def test_topic_update(self):
"""
create update
diff --git a/spirit/tests/tests_topic_favorite.py b/tests/tests_topic_favorite.py
similarity index 100%
rename from spirit/tests/tests_topic_favorite.py
rename to tests/tests_topic_favorite.py
diff --git a/tests/tests_topic_moderate.py b/tests/tests_topic_moderate.py
new file mode 100644
index 000000000..9c2d03e7b
--- /dev/null
+++ b/tests/tests_topic_moderate.py
@@ -0,0 +1,186 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+from django.test import TestCase
+from django.core.cache import cache
+from django.core.urlresolvers import reverse
+
+from . import utils
+
+from spirit.models.comment import CLOSED, UNCLOSED, PINNED, UNPINNED
+from spirit.models.topic import Topic
+from spirit.signals.topic_moderate import topic_post_moderate
+
+
+class TopicViewTest(TestCase):
+
+ def setUp(self):
+ cache.clear()
+ self.user = utils.create_user()
+
+ def test_topic_moderate_delete(self):
+ """
+ delete topic
+ """
+ utils.login(self)
+ self.user.is_moderator = True
+ self.user.save()
+
+ category = utils.create_category()
+ topic = utils.create_topic(category)
+ form_data = {}
+ response = self.client.post(reverse('spirit:topic-delete', kwargs={'pk': topic.pk, }),
+ form_data)
+ expected_url = topic.get_absolute_url()
+ self.assertRedirects(response, expected_url, status_code=302)
+ self.assertTrue(Topic.objects.get(pk=topic.pk).is_removed)
+
+ def test_topic_moderate_undelete(self):
+ """
+ undelete topic
+ """
+ utils.login(self)
+ self.user.is_moderator = True
+ self.user.save()
+
+ category = utils.create_category()
+ topic = utils.create_topic(category, is_removed=True)
+ form_data = {}
+ response = self.client.post(reverse('spirit:topic-undelete', kwargs={'pk': topic.pk, }),
+ form_data)
+ expected_url = topic.get_absolute_url()
+ self.assertRedirects(response, expected_url, status_code=302)
+ self.assertFalse(Topic.objects.get(pk=topic.pk).is_removed)
+
+ def test_topic_moderate_lock(self):
+ """
+ topic lock
+ """
+ def topic_post_moderate_handler(sender, user, topic, action, **kwargs):
+ self._moderate = [repr(user._wrapped), repr(topic), action]
+ topic_post_moderate.connect(topic_post_moderate_handler)
+
+ utils.login(self)
+ self.user.is_moderator = True
+ self.user.save()
+
+ category = utils.create_category()
+ topic = utils.create_topic(category)
+ form_data = {}
+ response = self.client.post(reverse('spirit:topic-lock', kwargs={'pk': topic.pk, }),
+ form_data)
+ expected_url = topic.get_absolute_url()
+ self.assertRedirects(response, expected_url, status_code=302)
+ self.assertTrue(Topic.objects.get(pk=topic.pk).is_closed)
+ self.assertEqual(self._moderate, [repr(self.user), repr(topic), CLOSED])
+
+ def test_topic_moderate_unlock(self):
+ """
+ unlock topic
+ """
+ def topic_post_moderate_handler(sender, user, topic, action, **kwargs):
+ self._moderate = [repr(user._wrapped), repr(topic), action]
+ topic_post_moderate.connect(topic_post_moderate_handler)
+
+ utils.login(self)
+ self.user.is_moderator = True
+ self.user.save()
+
+ category = utils.create_category()
+ topic = utils.create_topic(category, is_closed=True)
+ form_data = {}
+ response = self.client.post(reverse('spirit:topic-unlock', kwargs={'pk': topic.pk, }),
+ form_data)
+ expected_url = topic.get_absolute_url()
+ self.assertRedirects(response, expected_url, status_code=302)
+ self.assertFalse(Topic.objects.get(pk=topic.pk).is_closed)
+ self.assertEqual(self._moderate, [repr(self.user), repr(topic), UNCLOSED])
+
+ def test_topic_moderate_pin(self):
+ """
+ topic pin
+ """
+ def topic_post_moderate_handler(sender, user, topic, action, **kwargs):
+ self._moderate = [repr(user._wrapped), repr(topic), action]
+ topic_post_moderate.connect(topic_post_moderate_handler)
+
+ utils.login(self)
+ self.user.is_moderator = True
+ self.user.save()
+
+ category = utils.create_category()
+ topic = utils.create_topic(category)
+ form_data = {}
+ response = self.client.post(reverse('spirit:topic-pin', kwargs={'pk': topic.pk, }),
+ form_data)
+ expected_url = topic.get_absolute_url()
+ self.assertRedirects(response, expected_url, status_code=302)
+ self.assertTrue(Topic.objects.get(pk=topic.pk).is_pinned)
+ self.assertEqual(self._moderate, [repr(self.user), repr(topic), PINNED])
+
+ def test_topic_moderate_unpin(self):
+ """
+ topic unpin
+ """
+ def topic_post_moderate_handler(sender, user, topic, action, **kwargs):
+ self._moderate = [repr(user._wrapped), repr(topic), action]
+ topic_post_moderate.connect(topic_post_moderate_handler)
+
+ utils.login(self)
+ self.user.is_moderator = True
+ self.user.save()
+
+ category = utils.create_category()
+ topic = utils.create_topic(category, is_pinned=True)
+ form_data = {}
+ response = self.client.post(reverse('spirit:topic-unpin', kwargs={'pk': topic.pk, }),
+ form_data)
+ expected_url = topic.get_absolute_url()
+ self.assertRedirects(response, expected_url, status_code=302)
+ self.assertFalse(Topic.objects.get(pk=topic.pk).is_pinned)
+ self.assertEqual(self._moderate, [repr(self.user), repr(topic), UNPINNED])
+
+ def test_topic_moderate_global_pin(self):
+ """
+ topic pin
+ """
+ def topic_post_moderate_handler(sender, user, topic, action, **kwargs):
+ self._moderate = [repr(user._wrapped), repr(topic), action]
+ topic_post_moderate.connect(topic_post_moderate_handler)
+
+ utils.login(self)
+ self.user.is_moderator = True
+ self.user.save()
+
+ category = utils.create_category()
+ topic = utils.create_topic(category)
+ form_data = {}
+ response = self.client.post(reverse('spirit:topic-global-pin', kwargs={'pk': topic.pk, }),
+ form_data)
+ expected_url = topic.get_absolute_url()
+ self.assertRedirects(response, expected_url, status_code=302)
+ self.assertTrue(Topic.objects.get(pk=topic.pk).is_globally_pinned)
+ self.assertEqual(self._moderate, [repr(self.user), repr(topic), PINNED])
+
+ def test_topic_moderate_global_unpin(self):
+ """
+ topic unpin
+ """
+ def topic_post_moderate_handler(sender, user, topic, action, **kwargs):
+ self._moderate = [repr(user._wrapped), repr(topic), action]
+ topic_post_moderate.connect(topic_post_moderate_handler)
+
+ utils.login(self)
+ self.user.is_moderator = True
+ self.user.save()
+
+ category = utils.create_category()
+ topic = utils.create_topic(category, is_globally_pinned=True)
+ form_data = {}
+ response = self.client.post(reverse('spirit:topic-global-unpin', kwargs={'pk': topic.pk, }),
+ form_data)
+ expected_url = topic.get_absolute_url()
+ self.assertRedirects(response, expected_url, status_code=302)
+ self.assertFalse(Topic.objects.get(pk=topic.pk).is_globally_pinned)
+ self.assertEqual(self._moderate, [repr(self.user), repr(topic), UNPINNED])
diff --git a/spirit/tests/tests_topic_notification.py b/tests/tests_topic_notification.py
similarity index 87%
rename from spirit/tests/tests_topic_notification.py
rename to tests/tests_topic_notification.py
index 4beb5377d..135c55db5 100644
--- a/spirit/tests/tests_topic_notification.py
+++ b/tests/tests_topic_notification.py
@@ -11,12 +11,17 @@
from django.core.cache import cache
from django.template import Template, Context
from django.utils import timezone
+from django.conf import settings
+
+from djconfig.utils import override_djconfig
from . import utils
from spirit.models.topic_private import TopicPrivate
-from spirit.models.topic_notification import TopicNotification, comment_posted, \
- COMMENT, MENTION, topic_private_post_create, topic_private_access_pre_create, topic_viewed
+from spirit.models.topic_notification import TopicNotification, COMMENT, MENTION
+from spirit.signals.comment import comment_posted
+from spirit.signals.topic import topic_viewed
+from spirit.signals.topic_private import topic_private_post_create, topic_private_access_pre_create
from spirit.forms.topic_notification import NotificationCreationForm, NotificationForm
from spirit.templatetags.tags.topic_notification import render_notification_form, has_topic_notifications
@@ -52,6 +57,21 @@ def test_topic_notification_list(self):
response = self.client.get(reverse('spirit:topic-notification-list'))
self.assertQuerysetEqual(response.context['notifications'], map(repr, [self.topic_notification, ]))
+ @override_djconfig(topics_per_page=1)
+ def test_topic_notification_list_paginate(self):
+ """
+ topic notification list paginated
+ """
+ topic2 = utils.create_topic(self.category)
+ comment2 = utils.create_comment(topic=topic2)
+ topic_notification2 = TopicNotification.objects.create(user=self.user, topic=topic2,
+ comment=comment2, is_active=True,
+ action=COMMENT)
+
+ utils.login(self)
+ response = self.client.get(reverse('spirit:topic-notification-list'))
+ self.assertQuerysetEqual(response.context['notifications'], map(repr, [topic_notification2, ]))
+
def test_topic_notification_list_show_private_topic(self):
"""
topic private in notification list
@@ -91,16 +111,16 @@ def test_topic_notification_list_dont_show_topic_removed_or_no_access(self):
topic_c = utils.create_topic(category=category_removed)
topic_d = utils.create_topic(category=subcategory)
topic_e = utils.create_topic(category=subcategory_removed)
- unread_a = TopicNotification.objects.create(user=self.user, topic=topic_a.topic,
- comment=self.comment, is_active=True, action=COMMENT)
- unread_b = TopicNotification.objects.create(user=self.user, topic=topic_b,
- comment=self.comment, is_active=True, action=COMMENT)
- unread_c = TopicNotification.objects.create(user=self.user, topic=topic_c,
- comment=self.comment, is_active=True, action=COMMENT)
- unread_d = TopicNotification.objects.create(user=self.user, topic=topic_d,
- comment=self.comment, is_active=True, action=COMMENT)
- unread_e = TopicNotification.objects.create(user=self.user, topic=topic_e,
- comment=self.comment, is_active=True, action=COMMENT)
+ TopicNotification.objects.create(user=self.user, topic=topic_a.topic,
+ comment=self.comment, is_active=True, action=COMMENT)
+ TopicNotification.objects.create(user=self.user, topic=topic_b,
+ comment=self.comment, is_active=True, action=COMMENT)
+ TopicNotification.objects.create(user=self.user, topic=topic_c,
+ comment=self.comment, is_active=True, action=COMMENT)
+ TopicNotification.objects.create(user=self.user, topic=topic_d,
+ comment=self.comment, is_active=True, action=COMMENT)
+ TopicNotification.objects.create(user=self.user, topic=topic_e,
+ comment=self.comment, is_active=True, action=COMMENT)
self.assertEqual(len(TopicNotification.objects.filter(user=self.user, is_active=True, is_read=False)), 5)
utils.login(self)
@@ -272,6 +292,10 @@ def test_notification_creation(self):
"""
create notification
"""
+ # Should be ready to suscribe (true)
+ form = NotificationCreationForm()
+ self.assertEqual(form.fields['is_active'].initial, True)
+
category = utils.create_category()
topic = utils.create_topic(category)
form_data = {'is_active': True, }
@@ -301,11 +325,7 @@ def test_notification(self):
self.assertEqual(form.is_valid(), True)
-class TopicNotificationSignalTest(TransactionTestCase):
-
- # Needed to work with migrations when using TransactionTestCase
- available_apps = ["spirit", ]
- serialized_rollback = True
+class TopicNotificationSignalTest(TestCase):
def setUp(self):
cache.clear()
@@ -458,16 +478,16 @@ def test_topic_notification_has_notifications_dont_count_topic_removed_or_no_acc
topic_c = utils.create_topic(category=category_removed)
topic_d = utils.create_topic(category=subcategory)
topic_e = utils.create_topic(category=subcategory_removed)
- unread_a = TopicNotification.objects.create(user=self.user, topic=topic_a.topic,
- comment=self.comment, is_active=True, action=COMMENT)
- unread_b = TopicNotification.objects.create(user=self.user, topic=topic_b,
- comment=self.comment, is_active=True, action=COMMENT)
- unread_c = TopicNotification.objects.create(user=self.user, topic=topic_c,
- comment=self.comment, is_active=True, action=COMMENT)
- unread_d = TopicNotification.objects.create(user=self.user, topic=topic_d,
- comment=self.comment, is_active=True, action=COMMENT)
- unread_e = TopicNotification.objects.create(user=self.user, topic=topic_e,
- comment=self.comment, is_active=True, action=COMMENT)
+ TopicNotification.objects.create(user=self.user, topic=topic_a.topic,
+ comment=self.comment, is_active=True, action=COMMENT)
+ TopicNotification.objects.create(user=self.user, topic=topic_b,
+ comment=self.comment, is_active=True, action=COMMENT)
+ TopicNotification.objects.create(user=self.user, topic=topic_c,
+ comment=self.comment, is_active=True, action=COMMENT)
+ TopicNotification.objects.create(user=self.user, topic=topic_d,
+ comment=self.comment, is_active=True, action=COMMENT)
+ TopicNotification.objects.create(user=self.user, topic=topic_e,
+ comment=self.comment, is_active=True, action=COMMENT)
self.assertEqual(len(TopicNotification.objects.filter(user=self.user, is_active=True, is_read=False)), 5)
self.assertFalse(has_topic_notifications(self.user))
@@ -476,7 +496,7 @@ def test_render_notification_form_notify(self):
"""
should display the form
"""
- out = Template(
+ Template(
"{% load spirit_tags %}"
"{% render_notification_form user topic %}"
).render(Context({'topic': self.topic, 'user': self.user}))
diff --git a/spirit/tests/tests_topic_poll.py b/tests/tests_topic_poll.py
similarity index 98%
rename from spirit/tests/tests_topic_poll.py
rename to tests/tests_topic_poll.py
index 221b29843..fa5db609f 100644
--- a/spirit/tests/tests_topic_poll.py
+++ b/tests/tests_topic_poll.py
@@ -276,7 +276,7 @@ def test_create_poll_choices_filled_but_deleted(self):
'choices-0-description': 'op1', 'choices-0-DELETE': "on",
'choices-1-description': 'op2', 'choices-1-DELETE': "on"}
form = TopicPollChoiceFormSet(can_delete=True, data=form_data)
- self.assertTrue(form.is_valid())
+ self.assertTrue(form.is_valid()) # not enough choices, but since we are NOT updating this is valid
self.assertFalse(form.is_filled())
def test_update_poll_choices_filled_but_deleted(self):
@@ -289,7 +289,7 @@ def test_update_poll_choices_filled_but_deleted(self):
'choices-0-description': 'op1', 'choices-0-DELETE': "on",
'choices-1-description': 'op2', 'choices-1-DELETE': "on"}
form = TopicPollChoiceFormSet(can_delete=True, data=form_data, instance=poll)
- self.assertTrue(form.is_valid())
+ self.assertFalse(form.is_valid()) # not enough choices
self.assertTrue(form.is_filled())
@@ -505,7 +505,7 @@ def test_render_poll_form_user(self):
should load initial or not
"""
poll_choice = TopicPollChoice.objects.create(poll=self.poll, description="op2")
- poll_vote = TopicPollVote.objects.create(user=self.user, choice=poll_choice)
+ TopicPollVote.objects.create(user=self.user, choice=poll_choice)
self.user.is_authenticated = lambda: True
context = render_poll_form(self.topic, self.user)
diff --git a/spirit/tests/tests_topic_private.py b/tests/tests_topic_private.py
similarity index 84%
rename from spirit/tests/tests_topic_private.py
rename to tests/tests_topic_private.py
index 5aa8e5eeb..8a1ec9485 100644
--- a/spirit/tests/tests_topic_private.py
+++ b/tests/tests_topic_private.py
@@ -12,6 +12,8 @@
from django.utils import six
from django.utils import timezone
+from djconfig.utils import override_djconfig
+
from . import utils
from spirit.models.category import Category
@@ -23,6 +25,7 @@
from spirit.models.comment import Comment
from spirit.signals.topic_private import topic_private_post_create, topic_private_access_pre_create
from spirit.models.topic import Topic
+from spirit.models.comment_bookmark import CommentBookmark
class TopicPrivateViewTest(TestCase):
@@ -100,9 +103,37 @@ def test_private_detail(self):
"""
utils.login(self)
private = utils.create_private_topic(user=self.user)
+
+ comment1 = utils.create_comment(topic=private.topic)
+ comment2 = utils.create_comment(topic=private.topic)
+
+ category = utils.create_category()
+ topic2 = utils.create_topic(category=category)
+ utils.create_comment(topic=topic2)
+
response = self.client.get(reverse('spirit:private-detail', kwargs={'topic_id': private.topic.pk,
'slug': private.topic.slug}))
+ self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['topic'], private.topic)
+ self.assertQuerysetEqual(response.context['comments'], map(repr, [comment1, comment2]))
+
+ @override_djconfig(comments_per_page=2)
+ def test_private_detail_view_paginate(self):
+ """
+ should display topic with comments, page 1
+ """
+ utils.login(self)
+ private = utils.create_private_topic(user=self.user)
+
+ comment1 = utils.create_comment(topic=private.topic)
+ comment2 = utils.create_comment(topic=private.topic)
+ utils.create_comment(topic=private.topic) # comment3
+
+ response = self.client.get(reverse('spirit:private-detail', kwargs={'topic_id': private.topic.pk,
+ 'slug': private.topic.slug}))
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['topic'], private.topic)
+ self.assertQuerysetEqual(response.context['comments'], map(repr, [comment1, comment2]))
def test_private_access_create(self):
"""
@@ -192,10 +223,10 @@ def test_private_list(self):
"""
private = utils.create_private_topic(user=self.user)
# dont show private topics from other users
- private2 = TopicPrivate.objects.create(user=self.user2, topic=private.topic)
+ TopicPrivate.objects.create(user=self.user2, topic=private.topic)
# dont show topics from other categories
category = utils.create_category()
- topic = utils.create_topic(category, user=self.user)
+ utils.create_topic(category, user=self.user)
utils.login(self)
response = self.client.get(reverse('spirit:private-list'))
@@ -217,6 +248,30 @@ def test_private_list_order_topics(self):
self.assertQuerysetEqual(response.context['topics'], map(repr, [private_b.topic, private_c.topic,
private_a.topic]))
+ def test_private_list_bookmarks(self):
+ """
+ private topic list with bookmarks
+ """
+ private = utils.create_private_topic(user=self.user)
+ bookmark = CommentBookmark.objects.create(topic=private.topic, user=self.user)
+
+ utils.login(self)
+ response = self.client.get(reverse('spirit:private-list'))
+ self.assertQuerysetEqual(response.context['topics'], [repr(private.topic), ])
+ self.assertEqual(response.context['topics'][0].bookmark, bookmark)
+
+ @override_djconfig(topics_per_page=1)
+ def test_private_list(self):
+ """
+ private topic list paginated
+ """
+ utils.create_private_topic(user=self.user)
+ private = utils.create_private_topic(user=self.user)
+
+ utils.login(self)
+ response = self.client.get(reverse('spirit:private-list'))
+ self.assertQuerysetEqual(response.context['topics'], [repr(private.topic), ])
+
def test_private_join(self):
"""
private topic join
@@ -284,14 +339,14 @@ def test_private_created_list(self):
private topic created list, shows only the private topics the user is no longer participating
"""
category = utils.create_category()
- regular_topic = utils.create_topic(category, user=self.user)
+ utils.create_topic(category, user=self.user)
# it's the owner, left the topic
private = utils.create_private_topic(user=self.user)
private.delete()
# has access and is the owner
- private2 = utils.create_private_topic(user=self.user)
+ utils.create_private_topic(user=self.user)
# does not has access
- private3 = utils.create_private_topic(user=self.user2)
+ utils.create_private_topic(user=self.user2)
# has access but it's not owner
private4 = utils.create_private_topic(user=self.user2)
TopicPrivate.objects.create(user=self.user, topic=private4.topic)
@@ -319,6 +374,20 @@ def test_private_created_list_order_topics(self):
self.assertQuerysetEqual(response.context['topics'], map(repr, [private_b.topic, private_c.topic,
private_a.topic]))
+ @override_djconfig(topics_per_page=1)
+ def test_private_created_list_paginate(self):
+ """
+ private topic created list paginated
+ """
+ private = utils.create_private_topic(user=self.user)
+ private.delete()
+ private2 = utils.create_private_topic(user=self.user)
+ private2.delete()
+
+ utils.login(self)
+ response = self.client.get(reverse('spirit:private-created-list'))
+ self.assertQuerysetEqual(response.context['topics'], map(repr, [private2.topic, ]))
+
class TopicPrivateFormTest(TestCase):
diff --git a/spirit/tests/tests_topic_unread.py b/tests/tests_topic_unread.py
similarity index 82%
rename from spirit/tests/tests_topic_unread.py
rename to tests/tests_topic_unread.py
index f4a70e7c5..568879218 100644
--- a/spirit/tests/tests_topic_unread.py
+++ b/tests/tests_topic_unread.py
@@ -5,10 +5,14 @@
from django.core.cache import cache
from django.test import TestCase, TransactionTestCase, RequestFactory
from django.core.urlresolvers import reverse
+from django.conf import settings
from . import utils
-from spirit.models.topic_unread import TopicUnread, topic_viewed, comment_posted
+from spirit.models.topic_unread import TopicUnread
+from spirit.signals.topic import topic_viewed
+from spirit.signals.comment import comment_posted
+from spirit.models.comment_bookmark import CommentBookmark
class TopicUnreadViewTest(TestCase):
@@ -71,11 +75,11 @@ def test_topic_unread_list_dont_show_removed_or_no_access(self):
topic_c = utils.create_topic(category=category_removed)
topic_d = utils.create_topic(category=subcategory)
topic_e = utils.create_topic(category=subcategory_removed)
- unread_a = TopicUnread.objects.create(user=self.user, topic=topic_a.topic, is_read=False)
- unread_b = TopicUnread.objects.create(user=self.user, topic=topic_b, is_read=False)
- unread_c = TopicUnread.objects.create(user=self.user, topic=topic_c, is_read=False)
- unread_d = TopicUnread.objects.create(user=self.user, topic=topic_d, is_read=False)
- unread_e = TopicUnread.objects.create(user=self.user, topic=topic_e, is_read=False)
+ TopicUnread.objects.create(user=self.user, topic=topic_a.topic, is_read=False)
+ TopicUnread.objects.create(user=self.user, topic=topic_b, is_read=False)
+ TopicUnread.objects.create(user=self.user, topic=topic_c, is_read=False)
+ TopicUnread.objects.create(user=self.user, topic=topic_d, is_read=False)
+ TopicUnread.objects.create(user=self.user, topic=topic_e, is_read=False)
utils.login(self)
response = self.client.get(reverse('spirit:topic-unread-list'))
@@ -107,12 +111,22 @@ def test_topic_unread_list_empty_page(self):
response = self.client.get(reverse('spirit:topic-unread-list') + "?topic_id=" + str(self.topic.pk))
self.assertEqual(response.status_code, 404)
+ def test_topic_unread_list_bookmarks(self):
+ """
+ topic unread list with bookmarks
+ """
+ TopicUnread.objects\
+ .filter(pk__in=[self.topic_unread.pk, self.topic_unread2.pk])\
+ .update(is_read=False)
+ bookmark = CommentBookmark.objects.create(topic=self.topic2, user=self.user)
+
+ utils.login(self)
+ response = self.client.get(reverse('spirit:topic-unread-list'))
+ self.assertQuerysetEqual(response.context['page'], map(repr, [self.topic2, self.topic]))
+ self.assertEqual(response.context['page'][0].bookmark, bookmark)
-class TopicUnreadSignalTest(TransactionTestCase): # since signal raises IntegrityError
- # Needed to work with migrations when using TransactionTestCase
- available_apps = ["spirit", ]
- serialized_rollback = True
+class TopicUnreadSignalTest(TestCase):
def setUp(self):
cache.clear()
diff --git a/spirit/tests/tests_user.py b/tests/tests_user.py
similarity index 84%
rename from spirit/tests/tests_user.py
rename to tests/tests_user.py
index 239389d59..6412615f0 100644
--- a/spirit/tests/tests_user.py
+++ b/tests/tests_user.py
@@ -12,6 +12,8 @@
from django.utils.translation import ugettext as _
from django.utils import timezone
+from djconfig.utils import override_djconfig
+
from . import utils
from spirit.forms.user import RegistrationForm, UserProfileForm, EmailChangeForm, ResendActivationForm
@@ -21,6 +23,7 @@
from spirit.models.user import User as UserModel
from spirit.models.topic import Topic
from spirit.models.comment import Comment
+from spirit.models.comment_bookmark import CommentBookmark
User = get_user_model()
@@ -132,6 +135,32 @@ def test_profile_topics_order(self):
'slug': self.user2.slug}))
self.assertQuerysetEqual(response.context['topics'], map(repr, [topic_b, topic_c, topic_a]))
+ def test_profile_topics_bookmarks(self):
+ """
+ profile user's topics with bookmarks
+ """
+ bookmark = CommentBookmark.objects.create(topic=self.topic, user=self.user)
+
+ utils.login(self)
+ response = self.client.get(reverse("spirit:profile-topics",
+ kwargs={'pk': self.user2.pk, 'slug': self.user2.slug}))
+ self.assertEqual(response.status_code, 200)
+ self.assertQuerysetEqual(response.context['topics'], [repr(self.topic), ])
+ self.assertEqual(response.context['topics'][0].bookmark, bookmark)
+
+ @override_djconfig(topics_per_page=1)
+ def test_profile_topics_paginate(self):
+ """
+ profile user's topics paginated
+ """
+ topic = utils.create_topic(self.category, user=self.user2)
+
+ utils.login(self)
+ response = self.client.get(reverse("spirit:profile-topics", kwargs={'pk': self.user2.pk,
+ 'slug': self.user2.slug}))
+ self.assertEqual(response.status_code, 200)
+ self.assertQuerysetEqual(response.context['topics'], [repr(topic), ])
+
def test_profile_topics_dont_show_removed_or_private(self):
"""
dont show private topics or removed
@@ -142,11 +171,11 @@ def test_profile_topics_dont_show_removed_or_private(self):
category_removed = utils.create_category(is_removed=True)
subcategory = utils.create_category(parent=category_removed)
subcategory_removed = utils.create_category(parent=category, is_removed=True)
- topic_a = utils.create_private_topic(user=self.user2)
- topic_b = utils.create_topic(category=category, user=self.user2, is_removed=True)
- topic_c = utils.create_topic(category=category_removed, user=self.user2)
- topic_d = utils.create_topic(category=subcategory, user=self.user2)
- topic_e = utils.create_topic(category=subcategory_removed, user=self.user2)
+ utils.create_private_topic(user=self.user2)
+ utils.create_topic(category=category, user=self.user2, is_removed=True)
+ utils.create_topic(category=category_removed, user=self.user2)
+ utils.create_topic(category=subcategory, user=self.user2)
+ utils.create_topic(category=subcategory_removed, user=self.user2)
utils.login(self)
response = self.client.get(reverse("spirit:profile-topics", kwargs={'pk': self.user2.pk,
@@ -170,7 +199,7 @@ def test_profile_comments(self):
"""
utils.login(self)
comment = utils.create_comment(user=self.user2, topic=self.topic)
- comment2 = utils.create_comment(user=self.user, topic=self.topic)
+ utils.create_comment(user=self.user, topic=self.topic)
response = self.client.get(reverse("spirit:profile-detail", kwargs={'pk': self.user2.pk,
'slug': self.user2.slug}))
self.assertEqual(response.status_code, 200)
@@ -193,6 +222,20 @@ def test_profile_comments_order(self):
'slug': self.user2.slug}))
self.assertQuerysetEqual(response.context['comments'], map(repr, [comment_b, comment_c, comment_a]))
+ @override_djconfig(comments_per_page=1)
+ def test_profile_comments_paginate(self):
+ """
+ profile user's comments paginated
+ """
+ utils.create_comment(user=self.user2, topic=self.topic)
+ comment = utils.create_comment(user=self.user2, topic=self.topic)
+
+ utils.login(self)
+ response = self.client.get(reverse("spirit:profile-detail", kwargs={'pk': self.user2.pk,
+ 'slug': self.user2.slug}))
+ self.assertEqual(response.status_code, 200)
+ self.assertQuerysetEqual(response.context['comments'], [repr(comment), ])
+
def test_profile_comments_dont_show_removed_or_private(self):
"""
dont show private topics or removed
@@ -206,11 +249,11 @@ def test_profile_comments_dont_show_removed_or_private(self):
topic_c = utils.create_topic(category=category_removed)
topic_d = utils.create_topic(category=subcategory)
topic_e = utils.create_topic(category=subcategory_removed)
- comment_a = utils.create_comment(user=self.user2, topic=topic_a.topic)
- comment_b = utils.create_comment(user=self.user2, topic=topic_b)
- comment_c = utils.create_comment(user=self.user2, topic=topic_c)
- comment_d = utils.create_comment(user=self.user2, topic=topic_d)
- comment_e = utils.create_comment(user=self.user2, topic=topic_e)
+ utils.create_comment(user=self.user2, topic=topic_a.topic)
+ utils.create_comment(user=self.user2, topic=topic_b)
+ utils.create_comment(user=self.user2, topic=topic_c)
+ utils.create_comment(user=self.user2, topic=topic_d)
+ utils.create_comment(user=self.user2, topic=topic_e)
utils.login(self)
response = self.client.get(reverse("spirit:profile-detail", kwargs={'pk': self.user2.pk,
@@ -236,7 +279,7 @@ def test_profile_likes(self):
comment = utils.create_comment(user=self.user, topic=self.topic)
comment2 = utils.create_comment(user=self.user2, topic=self.topic)
like = CommentLike.objects.create(user=self.user2, comment=comment)
- like2 = CommentLike.objects.create(user=self.user, comment=comment2)
+ CommentLike.objects.create(user=self.user, comment=comment2)
response = self.client.get(reverse("spirit:profile-likes", kwargs={'pk': self.user2.pk,
'slug': self.user2.slug}))
self.assertEqual(response.status_code, 200)
@@ -251,7 +294,7 @@ def test_profile_likes_order(self):
comment_b = utils.create_comment(user=self.user, topic=self.topic)
comment_c = utils.create_comment(user=self.user, topic=self.topic)
like_a = CommentLike.objects.create(user=self.user2, comment=comment_a)
- like_b = CommentLike.objects.create(user=self.user2, comment=comment_b)
+ CommentLike.objects.create(user=self.user2, comment=comment_b)
like_c = CommentLike.objects.create(user=self.user2, comment=comment_c)
CommentLike.objects.filter(pk=like_a.pk).update(date=timezone.now() - datetime.timedelta(days=10))
@@ -280,11 +323,11 @@ def test_profile_likes_dont_show_removed_or_private(self):
comment_c = utils.create_comment(user=self.user, topic=topic_c)
comment_d = utils.create_comment(user=self.user, topic=topic_d)
comment_e = utils.create_comment(user=self.user, topic=topic_e)
- like_a = CommentLike.objects.create(user=self.user2, comment=comment_a)
- like_b = CommentLike.objects.create(user=self.user2, comment=comment_b)
- like_c = CommentLike.objects.create(user=self.user2, comment=comment_c)
- like_d = CommentLike.objects.create(user=self.user2, comment=comment_d)
- like_e = CommentLike.objects.create(user=self.user2, comment=comment_e)
+ CommentLike.objects.create(user=self.user2, comment=comment_a)
+ CommentLike.objects.create(user=self.user2, comment=comment_b)
+ CommentLike.objects.create(user=self.user2, comment=comment_c)
+ CommentLike.objects.create(user=self.user2, comment=comment_d)
+ CommentLike.objects.create(user=self.user2, comment=comment_e)
utils.login(self)
response = self.client.get(reverse("spirit:profile-likes", kwargs={'pk': self.user2.pk,
@@ -302,6 +345,22 @@ def test_profile_likes_invalid_slug(self):
'slug': self.user2.slug})
self.assertRedirects(response, expected_url, status_code=301)
+ @override_djconfig(comments_per_page=1)
+ def test_profile_likes_paginate(self):
+ """
+ profile user's likes paginate
+ """
+ comment = utils.create_comment(user=self.user2, topic=self.topic)
+ comment2 = utils.create_comment(user=self.user2, topic=self.topic)
+ CommentLike.objects.create(user=self.user2, comment=comment)
+ like = CommentLike.objects.create(user=self.user2, comment=comment2)
+
+ utils.login(self)
+ response = self.client.get(reverse("spirit:profile-likes", kwargs={'pk': self.user2.pk,
+ 'slug': self.user2.slug}))
+ self.assertEqual(response.status_code, 200)
+ self.assertQuerysetEqual(response.context['comments'], [repr(like.comment), ])
+
def test_profile_update(self):
"""
profile update
@@ -324,21 +383,29 @@ def test_login_rate_limit(self):
test rate limit 5/5m
"""
form_data = {'username': self.user.email, 'password': "badpassword"}
- url = reverse('spirit:user-login') + "?next=/path/"
- for _ in range(6):
- response = self.client.post(url, form_data)
- self.assertRedirects(response, url, status_code=302)
+
+ for attempt in range(6):
+ if attempt < 5:
+ url = reverse('spirit:user-login')
+ response = self.client.post(url, form_data)
+ self.assertTemplateUsed(response, 'spirit/user/login.html')
+ else:
+ url = reverse('spirit:user-login') + "?next=/path/"
+ response = self.client.post(url, form_data)
+ self.assertRedirects(response, url, status_code=302)
def test_custom_reset_password(self):
"""
test rate limit 5/5m
"""
form_data = {'email': "bademail@bad.com", }
- for _ in range(6):
- response = self.client.post(reverse('spirit:password-reset'),
- form_data)
- expected_url = reverse("spirit:password-reset")
- self.assertRedirects(response, expected_url, status_code=302)
+ for attempt in range(6):
+ response = self.client.post(reverse('spirit:password-reset'), form_data)
+ if attempt < 5:
+ expected_url = reverse("spirit:password-reset-done")
+ else:
+ expected_url = reverse("spirit:password-reset")
+ self.assertRedirects(response, expected_url, status_code=302)
def test_password_reset_confirm(self):
"""
@@ -382,6 +449,7 @@ def test_registration_activation(self):
"""
registration activation
"""
+ self.user.is_verified = False
self.user.is_active = False
self.user.save()
token = UserActivationTokenGenerator().generate(self.user)
@@ -393,14 +461,14 @@ def test_registration_activation(self):
def test_registration_activation_invalid(self):
"""
- Activation token should expire after first login
+ Activation token should not work if user is verified
ActiveUserMiddleware required
"""
- self.user.last_login = self.user.last_login - datetime.timedelta(hours=1)
+ self.user.is_verified = False
token = UserActivationTokenGenerator().generate(self.user)
utils.login(self)
- User.objects.filter(pk=self.user.pk).update(is_active=False)
+ User.objects.filter(pk=self.user.pk).update(is_active=False, is_verified=True)
response = self.client.get(reverse('spirit:registration-activation', kwargs={'pk': self.user.pk,
'token': token}))
expected_url = reverse("spirit:user-login")
@@ -473,9 +541,9 @@ def test_resend_activation_email(self):
def test_resend_activation_email_invalid_previously_logged_in(self):
"""
- resend_activation_email invalid if last_ip was set
+ resend_activation_email invalid if is_verified was set
"""
- user = utils.create_user(password="foo", last_ip="1.1.1.1")
+ user = utils.create_user(password="foo", is_verified=True)
form_data = {'email': user.email,
'password': "foo"}
@@ -488,7 +556,7 @@ def test_resend_activation_email_invalid_email(self):
"""
resend_activation_email invalid password
"""
- user = utils.create_user(password="foo")
+ utils.create_user(password="foo")
form_data = {'email': "bad@foo.com", }
response = self.client.post(reverse('spirit:resend-activation'),
diff --git a/spirit/tests/tests_utils.py b/tests/tests_utils.py
similarity index 98%
rename from spirit/tests/tests_utils.py
rename to tests/tests_utils.py
index c45302049..2c42a60f1 100644
--- a/spirit/tests/tests_utils.py
+++ b/tests/tests_utils.py
@@ -322,7 +322,7 @@ def test_markdown_mentions_dict(self):
"""
comment = "@nitely, @esteban"
md = Markdown(escape=True, hard_wrap=True)
- comment_md = md.render(comment)
+ md.render(comment)
# mentions get dianmically added on MentionifyExtension
self.assertDictEqual(md.get_mentions(), {'nitely': self.user,
'esteban': self.user2})
@@ -463,22 +463,21 @@ def test_user_activation_token_generator(self):
"""
Validate if user can be activated
"""
- self.user.last_login = self.user.last_login - datetime.timedelta(hours=1)
+ self.user.is_verified = False
activation_token = UserActivationTokenGenerator()
token = activation_token.generate(self.user)
self.assertTrue(activation_token.is_valid(self.user, token))
self.assertFalse(activation_token.is_valid(self.user, "bad token"))
+ # Invalid after verification
+ self.user.is_verified = True
+ self.assertFalse(activation_token.is_valid(self.user, token))
+
# Invalid for different user
user2 = test_utils.create_user()
self.assertFalse(activation_token.is_valid(user2, token))
- # Invalid after login
- test_utils.login(self)
- user = test_utils.User.objects.get(pk=self.user.pk)
- self.assertFalse(activation_token.is_valid(user, token))
-
def test_user_email_change_token_generator(self):
"""
Email change
diff --git a/spirit/tests/tests_utils_models.py b/tests/tests_utils_models.py
similarity index 94%
rename from spirit/tests/tests_utils_models.py
rename to tests/tests_utils_models.py
index 184e368dd..83b3ecf84 100644
--- a/spirit/tests/tests_utils_models.py
+++ b/tests/tests_utils_models.py
@@ -4,7 +4,7 @@
from django.test import TestCase
-from spirit.tests.models.auto_slug import AutoSlugPopulateFromModel, AutoSlugModel, AutoSlugDefaultModel, \
+from .models.auto_slug import AutoSlugPopulateFromModel, AutoSlugModel, AutoSlugDefaultModel, \
AutoSlugBadPopulateFromModel
diff --git a/spirit/tests/tests_utils_paginator.py b/tests/tests_utils_paginator.py
similarity index 66%
rename from spirit/tests/tests_utils_paginator.py
rename to tests/tests_utils_paginator.py
index dd55300ef..9a59d0368 100644
--- a/spirit/tests/tests_utils_paginator.py
+++ b/tests/tests_utils_paginator.py
@@ -10,13 +10,13 @@
from django.core.paginator import Page, Paginator
from . import utils
-from spirit.models.comment import Comment
+from spirit.models.comment import Comment
from spirit.utils.paginator.yt_paginator import YTPaginator, InvalidPage, YTPage
from spirit.utils import paginator
-from spirit.utils.paginator import infinite_paginator
-from spirit.templatetags.tags.utils.paginator import yt_paginator_autopaginate, paginator_autopaginate, \
- render_yt_paginator, render_paginator
+from spirit.utils.paginator import infinite_paginator, paginate, yt_paginate
+from spirit.templatetags.tags.utils.paginator import render_paginator
+from spirit.templatetags.tags.utils import paginator as ttag_paginator
class UtilsPaginatorTest(TestCase):
@@ -171,81 +171,64 @@ class UtilsYTPaginatorTemplateTagsTests(TestCase):
def setUp(self):
cache.clear()
- def tests_yt_paginator_autopaginate_tag(self):
- """
- Minimal test to check it works
- """
- req = RequestFactory().get('/')
- out = Template(
- "{% load spirit_tags %}"
- "{% yt_paginator_autopaginate items per_page=5 as page %}"
- "{% for p in page %}"
- "{{ p }}"
- "{% endfor %}"
- ).render(Context({'request': req, 'items': list(range(0, 20)), }))
- self.assertEqual(out, "01234")
-
- def tests_yt_paginator_autopaginate(self):
+ def tests_yt_paginate(self):
# first page
- req = RequestFactory().get('/')
- context = {'request': req, }
items = list(range(0, 20))
- page = yt_paginator_autopaginate(context, items, per_page=10, page_var="val")
+ page = yt_paginate(items, per_page=10)
self.assertIsInstance(page, YTPage)
self.assertEqual(list(page), items[:10])
# second page
- req = RequestFactory().get('/?val=2')
- context = {'request': req, }
- page = yt_paginator_autopaginate(context, items, per_page=10, page_var="val")
+ page = yt_paginate(items, per_page=10, page_number=2)
self.assertEqual(list(page), items[10:20])
# invalid page
- req = RequestFactory().get('/?val=3')
- context = {'request': req, }
- self.assertRaises(Http404, yt_paginator_autopaginate,
- context, items, per_page=10, page_var="val")
+ self.assertRaises(Http404, yt_paginate,
+ items, per_page=10, page_number=99)
# empty first page
- req = RequestFactory().get('/')
- context = {'request': req, }
- page = yt_paginator_autopaginate(context, [], per_page=10, page_var="val")
+ page = yt_paginate([], per_page=10)
self.assertListEqual(list(page), [])
- def tests_render_yt_paginator_tag(self):
- """
- Minimal test to check it works
- """
- req = RequestFactory().get('/')
- items = list(range(0, 20))
- page = YTPaginator(items, per_page=10).page(1)
- out = Template(
- "{% load spirit_tags %}"
- "{% render_yt_paginator page %}"
- ).render(Context({'request': req, 'page': page, }))
-
def tests_render_yt_paginator(self):
+ def mock_render(template, context):
+ return template, context
+
req = RequestFactory().get('/')
context = {'request': req, }
items = list(range(0, 20))
page = YTPaginator(items, per_page=10).page(1)
- res = render_yt_paginator(context, page)
- self.assertDictEqual(res, {"page": page,
- "page_var": 'page',
- "hashtag": '',
- "extra_query": ''})
+
+ org_render, ttag_paginator.render_to_string = ttag_paginator.render_to_string, mock_render
+ try:
+ template, context2 = render_paginator(context, page)
+ self.assertDictEqual(context2, {"page": page,
+ "page_var": 'page',
+ "hashtag": '',
+ "extra_query": ''})
+ self.assertEqual(template, "spirit/paginator/_yt_paginator.html")
+ finally:
+ ttag_paginator.render_to_string = org_render
def tests_render_yt_paginator_extra(self):
+ def mock_render(template, context):
+ return template, context
+
req = RequestFactory().get('/?foo_page=1&extra=foo')
context = {'request': req, }
items = list(range(0, 20))
page = YTPaginator(items, per_page=10).page(1)
- res = render_yt_paginator(context, page, page_var='foo_page', hashtag="c20")
- self.assertDictEqual(res, {"page": page,
- "page_var": 'foo_page',
- "hashtag": '#c20',
- "extra_query": '&extra=foo'})
+ org_render, ttag_paginator.render_to_string = ttag_paginator.render_to_string, mock_render
+ try:
+ template, context2 = render_paginator(context, page, page_var='foo_page', hashtag="c20")
+ self.assertDictEqual(context2, {"page": page,
+ "page_var": 'foo_page',
+ "hashtag": '#c20',
+ "extra_query": '&extra=foo'})
+ self.assertEqual(template, "spirit/paginator/_yt_paginator.html")
+ finally:
+ ttag_paginator.render_to_string = org_render
class UtilsPaginatorTemplateTagsTests(TestCase):
@@ -253,45 +236,23 @@ class UtilsPaginatorTemplateTagsTests(TestCase):
def setUp(self):
cache.clear()
- def tests_paginator_autopaginate_tag(self):
- """
- Minimal test to check it works
- """
- req = RequestFactory().get('/')
- out = Template(
- "{% load spirit_tags %}"
- "{% paginator_autopaginate items per_page=5 as page %}"
- "{% for p in page %}"
- "{{ p }}"
- "{% endfor %}"
- ).render(Context({'request': req, 'items': list(range(0, 20)), }))
- self.assertEqual(out, "01234")
-
- def tests_paginator_autopaginate(self):
+ def tests_paginate(self):
# first page
- req = RequestFactory().get('/')
- context = {'request': req, }
items = list(range(0, 20))
- page = paginator_autopaginate(context, items, per_page=10, page_var="val")
+ page = paginate(items, per_page=10)
self.assertIsInstance(page, Page)
self.assertEqual(list(page), items[:10])
# second page
- req = RequestFactory().get('/?val=2')
- context = {'request': req, }
- page = paginator_autopaginate(context, items, per_page=10, page_var="val")
+ page = paginate(items, per_page=10, page_number=2)
self.assertEqual(list(page), items[10:20])
# invalid page
- req = RequestFactory().get('/?val=3')
- context = {'request': req, }
- self.assertRaises(Http404, paginator_autopaginate,
- context, items, per_page=10, page_var="val")
+ self.assertRaises(Http404, paginate,
+ items, per_page=10, page_number=99)
# empty first page
- req = RequestFactory().get('/')
- context = {'request': req, }
- page = paginator_autopaginate(context, [], per_page=10, page_var="val")
+ page = paginate([], per_page=10)
self.assertListEqual(list(page), [])
def tests_render_paginator_tag(self):
@@ -301,29 +262,47 @@ def tests_render_paginator_tag(self):
req = RequestFactory().get('/')
items = list(range(0, 20))
page = Paginator(items, per_page=10).page(1)
- out = Template(
+ Template(
"{% load spirit_tags %}"
"{% render_paginator page %}"
).render(Context({'request': req, 'page': page, }))
def tests_render_paginator(self):
+ def mock_render(template, context):
+ return template, context
+
req = RequestFactory().get('/')
context = {'request': req, }
items = list(range(0, 20))
page = Paginator(items, per_page=10).page(1)
- res = render_paginator(context, page)
- self.assertDictEqual(res, {"page": page,
- "page_var": 'page',
- "hashtag": '',
- "extra_query": ''})
+
+ org_render, ttag_paginator.render_to_string = ttag_paginator.render_to_string, mock_render
+ try:
+ template, context2 = render_paginator(context, page)
+ self.assertDictEqual(context2, {"page": page,
+ "page_var": 'page',
+ "hashtag": '',
+ "extra_query": ''})
+ self.assertEqual(template, "spirit/paginator/_paginator.html")
+ finally:
+ ttag_paginator.render_to_string = org_render
def tests_render_paginator_extra(self):
+ def mock_render(template, context):
+ return template, context
+
req = RequestFactory().get('/?foo_page=1&extra=foo')
context = {'request': req, }
items = list(range(0, 20))
page = Paginator(items, per_page=10).page(1)
- res = render_paginator(context, page, page_var='foo_page', hashtag="c20")
- self.assertDictEqual(res, {"page": page,
- "page_var": 'foo_page',
- "hashtag": '#c20',
- "extra_query": '&extra=foo'})
+
+ org_render, ttag_paginator.render_to_string = ttag_paginator.render_to_string, mock_render
+ try:
+ template, context2 = render_paginator(context, page, page_var='foo_page', hashtag="c20")
+ self.assertDictEqual(context2, {"page": page,
+ "page_var": 'foo_page',
+ "hashtag": '#c20',
+ "extra_query": '&extra=foo'})
+ self.assertEqual(template, "spirit/paginator/_paginator.html")
+ finally:
+ ttag_paginator.render_to_string = org_render
diff --git a/spirit/tests/tests_utils_ratelimit.py b/tests/tests_utils_ratelimit.py
similarity index 100%
rename from spirit/tests/tests_utils_ratelimit.py
rename to tests/tests_utils_ratelimit.py
diff --git a/tests/urls.py b/tests/urls.py
new file mode 100644
index 000000000..b4de8096f
--- /dev/null
+++ b/tests/urls.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+from django.conf.urls import patterns, include, url
+from django.contrib import admin
+
+# Override admin login for security purposes
+from django.contrib.auth.decorators import login_required
+admin.site.login = login_required(admin.site.login)
+
+
+urlpatterns = patterns('',
+ url(r'^', include('spirit.urls')),
+ url(r'^admin/', include(admin.site.urls)),
+ )
diff --git a/spirit/tests/utils.py b/tests/utils.py
similarity index 78%
rename from spirit/tests/utils.py
rename to tests/utils.py
index 558d0947f..92f134aeb 100644
--- a/spirit/tests/utils.py
+++ b/tests/utils.py
@@ -15,7 +15,7 @@
def create_user(**kwargs):
if 'username' not in kwargs:
- kwargs['username'] = "foo%d" % User.objects.all().count()
+ kwargs['username'] = "user_foo%d" % User.objects.all().count()
if 'email' not in kwargs:
kwargs['email'] = "%s@bar.com" % kwargs['username']
@@ -28,12 +28,10 @@ def create_user(**kwargs):
def create_topic(category, **kwargs):
if 'user' not in kwargs:
- username = "foo%d" % User.objects.all().count()
- email = "%s@bar.com" % username
- kwargs['user'] = User.objects.create_user(username=username, password="bar", email=email)
+ kwargs['user'] = create_user()
if 'title' not in kwargs:
- kwargs['title'] = "foo%d" % Topic.objects.all().count()
+ kwargs['title'] = "topic_foo%d" % Topic.objects.all().count()
return Topic.objects.create(category=category, **kwargs)
@@ -48,21 +46,21 @@ def create_private_topic(**kwargs):
def create_category(**kwargs):
if 'title' not in kwargs:
- kwargs['title'] = "foo%d" % Category.objects.all().count()
+ kwargs['title'] = "category_foo%d" % Category.objects.all().count()
return Category.objects.create(**kwargs)
def create_subcategory(category, **kwargs):
if 'title' not in kwargs:
- kwargs['title'] = "foo%d" % Category.objects.all().count()
+ kwargs['title'] = "subcategory_foo%d" % Category.objects.all().count()
return Category.objects.create(parent=category, **kwargs)
def create_comment(**kwargs):
if 'comment' not in kwargs:
- kwargs['comment'] = "foobar%d" % Comment.objects.all().count()
+ kwargs['comment'] = "comment_foobar%d" % Comment.objects.all().count()
if 'comment_html' not in kwargs:
kwargs['comment_html'] = kwargs['comment']
{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/spirit/templates/spirit/topic/topic_publish.html b/spirit/templates/spirit/topic/topic_publish.html index be102a403..96c49b549 100644 --- a/spirit/templates/spirit/topic/topic_publish.html +++ b/spirit/templates/spirit/topic/topic_publish.html @@ -37,17 +37,14 @@- {% trans "Poll" %}:
- {% if not pform.errors and not pformset|has_errors %}
+ {% if not pform.errors and not pformset.has_errors %}
- {% trans "Add poll" %}
{% endif %}
- -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/spirit/templates/spirit/topic/topics_active.html b/spirit/templates/spirit/topic/topics_active.html index d6a928626..5ed1b3253 100644 --- a/spirit/templates/spirit/topic/topics_active.html +++ b/spirit/templates/spirit/topic/topics_active.html @@ -1,6 +1,6 @@ {% extends "spirit/_base.html" %} -{% load i18n %} +{% load spirit_tags i18n %} {% block title %}{% trans "Latest active topics" %}{% endblock %} @@ -8,6 +8,8 @@ {% include "spirit/topic/_top_bar.html" with category=None categories=categories %} - {% include "spirit/topic/_render_page_list.html" %} + {% include "spirit/topic/_render_list.html" %} -{% endblock %} \ No newline at end of file + {% render_paginator topics %} + +{% endblock %} diff --git a/spirit/templates/spirit/topic_notification/_render_list.html b/spirit/templates/spirit/topic_notification/_render_list.html index 641c2d0cf..260757569 100644 --- a/spirit/templates/spirit/topic_notification/_render_list.html +++ b/spirit/templates/spirit/topic_notification/_render_list.html @@ -1,6 +1,6 @@ {% load i18n %} - {% for n in page %} + {% for n in notifications %}
{% url "spirit:profile-detail" pk=n.comment.user.pk slug=n.comment.user.slug as url_profile %}
{% url "spirit:comment-find" pk=n.comment.pk as url_topic %}
@@ -19,4 +19,4 @@
{% empty %}
{% trans "Publish topic" %}
{% trans "There are no notifications, yet" %}
- {% endfor %} \ No newline at end of file + {% endfor %} diff --git a/spirit/templates/spirit/topic_notification/list.html b/spirit/templates/spirit/topic_notification/list.html index 19c27442e..7a01c15a3 100644 --- a/spirit/templates/spirit/topic_notification/list.html +++ b/spirit/templates/spirit/topic_notification/list.html @@ -8,12 +8,10 @@{% trans "Notifications" %}
- {% yt_paginator_autopaginate notifications as page %} -{% trans "Unread notifications" %}
{% trans "There are no new notifications" %}.
{% endif %} @@ -23,4 +23,4 @@{% trans "Unread notifications" %}
{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/spirit/templates/spirit/topic_poll/poll_update.html b/spirit/templates/spirit/topic_poll/poll_update.html index 5d23b124f..aa3e42418 100644 --- a/spirit/templates/spirit/topic_poll/poll_update.html +++ b/spirit/templates/spirit/topic_poll/poll_update.html @@ -11,13 +11,9 @@{% trans "Update poll" %}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/spirit/templates/spirit/topic_private/private_created_list.html b/spirit/templates/spirit/topic_private/private_created_list.html index e84701697..a9b019820 100644 --- a/spirit/templates/spirit/topic_private/private_created_list.html +++ b/spirit/templates/spirit/topic_private/private_created_list.html @@ -6,13 +6,11 @@ {% block content %} - {% yt_paginator_autopaginate topics as page %} - {% include "spirit/topic_private/_top_bar.html" with active="left" %}{{ topic.title }}
{# this can be *included* here and in topic_detail #}{{ topic.title }}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/spirit/templates/spirit/topic_private/private_list.html b/spirit/templates/spirit/topic_private/private_list.html index 4c98694b2..96b85f130 100644 --- a/spirit/templates/spirit/topic_private/private_list.html +++ b/spirit/templates/spirit/topic_private/private_list.html @@ -1,6 +1,6 @@ {% extends "spirit/_base.html" %} -{% load i18n %} +{% load spirit_tags i18n %} {% block title %}{% trans "Private topics" %}{% endblock %} @@ -8,6 +8,8 @@ {% include "spirit/topic_private/_top_bar.html" with active="participating" %} - {% include "spirit/topic/_render_page_list.html" %} + {% include "spirit/topic/_render_list.html" %} -{% endblock %} \ No newline at end of file + {% render_paginator topics %} + +{% endblock %} diff --git a/spirit/templates/spirit/user/_render_comments_list.html b/spirit/templates/spirit/user/_render_comments_list.html index 7f85ccd13..430c53be5 100644 --- a/spirit/templates/spirit/user/_render_comments_list.html +++ b/spirit/templates/spirit/user/_render_comments_list.html @@ -2,7 +2,7 @@