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 %} -
- {% for c in page %} + {% for c in comments %} -
+
{% if not c.is_removed %} @@ -150,4 +146,4 @@ }); - \ No newline at end of file + diff --git a/spirit/templates/spirit/comment_history/detail.html b/spirit/templates/spirit/comment_history/detail.html index 11333ba31..e11fa792e 100644 --- a/spirit/templates/spirit/comment_history/detail.html +++ b/spirit/templates/spirit/comment_history/detail.html @@ -12,9 +12,7 @@

{% trans "Comment history" %}

- {% yt_paginator_autopaginate comments as page %} - - {% for c in page %} + {% for c in comments %}
@@ -46,7 +44,7 @@

{% trans "Comment history" %}

- {% render_yt_paginator page %} + {% render_paginator comments %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/spirit/templates/spirit/search/search.html b/spirit/templates/spirit/search/search.html index 3ccd8bfe8..2462f2705 100644 --- a/spirit/templates/spirit/search/search.html +++ b/spirit/templates/spirit/search/search.html @@ -18,16 +18,15 @@

{% 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 #}
- {% for t in page %} + {% for t in topics %}
- {% if t.is_pinned %} + {% if t.is_pinned or t.is_globally_pinned %} {% endif %} {% if t.is_closed %} @@ -45,4 +41,4 @@

{% trans "There are no topics here, yet" %}

{% endfor %} -
\ No newline at end of file +
diff --git a/spirit/templates/spirit/topic/_render_page_list.html b/spirit/templates/spirit/topic/_render_page_list.html deleted file mode 100644 index a62586c71..000000000 --- a/spirit/templates/spirit/topic/_render_page_list.html +++ /dev/null @@ -1,7 +0,0 @@ -{% load spirit_tags %} - -{% yt_paginator_autopaginate topics as page %} - -{% include "spirit/topic/_render_list.html" %} - -{% render_yt_paginator page %} \ No newline at end of file diff --git a/spirit/templates/spirit/topic/topic_detail.html b/spirit/templates/spirit/topic/topic_detail.html index bc4935700..221b27b02 100644 --- a/spirit/templates/spirit/topic/topic_detail.html +++ b/spirit/templates/spirit/topic/topic_detail.html @@ -17,7 +17,7 @@

- {% if topic.is_pinned %} + {% if topic.is_pinned or topic.is_globally_pinned %} {% endif %} {% if topic.is_closed %} @@ -58,6 +58,12 @@

{% else %}
  • {% trans "Pin topic" %}
  • {% endif %} + + {% if topic.is_globally_pinned %} +
  • {% trans "Unpin topic globally" %}
  • + {% else %} +
  • {% trans "Pin topic globally" %}
  • + {% endif %}

    @@ -74,13 +80,10 @@

    {% 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 @@

    {% 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 "Publish topic" %}

    {% trans "Poll" %}:
    - {% if not pform.errors and not pformset|has_errors %} + {% if not pform.errors and not pformset.has_errors %}
    {% trans "Add poll" %}
    {% endif %}
    -
    +
    {% include "spirit/_form.html" with form=pform %} - {{ pformset.management_form }} - {% for pfs in pformset %} - {% include "spirit/_form.html" with form=pfs %} - {% endfor %} + {% include "spirit/_formset.html" with formset=pformset %}
    {# comment #} @@ -62,4 +59,4 @@

    {% trans "Publish topic" %}

    -{% 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 "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 %} -
    {% 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 @@

    {% trans "Unread notifications" %}

    {% if page %} - {% include "spirit/topic_notification/_render_list.html" %} + {% include "spirit/topic_notification/_render_list.html" with notifications=page %} {% else %}

    {% 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" %}

    {% csrf_token %} {% include "spirit/_form.html" %} - - {{ formset.management_form }} - {% for f in formset %} - {% include "spirit/_form.html" with form=f %} - {% endfor %} + {% include "spirit/_formset.html" %}
    -{% 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" %}
    - {% for t in page %} + {% for t in topics %} @@ -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 %} - @@ -41,7 +38,7 @@

    {{ topic.title }}

    {# this can be *included* here and in topic_detail #}
    - {% render_paginator page %} + {% render_paginator comments %}
    {% render_notification_form user=user topic=topic %} @@ -74,4 +71,4 @@

    {{ 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 @@
    - {% 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']