diff --git a/barbeque/cms/toolbar.py b/barbeque/cms/toolbar.py index 90ed3ee..be67d8c 100644 --- a/barbeque/cms/toolbar.py +++ b/barbeque/cms/toolbar.py @@ -1,11 +1,15 @@ -from cms.cms_toolbars import ( - ADMIN_MENU_IDENTIFIER, PAGE_MENU_IDENTIFIER) +from django.utils.encoding import force_text + +from cms.cms_toolbars import ADMIN_MENU_IDENTIFIER, PAGE_MENU_IDENTIFIER +from cms.extensions.toolbar import ExtensionToolbar from cms.toolbar_base import CMSToolbar from cms.toolbar_pool import toolbar_pool from cms.toolbar.items import SideframeItem, ModalItem, SubMenu +@toolbar_pool.register class ForceModalDialogToolbar(CMSToolbar): + def rebuild_menu(self, menu): items = [] for item in menu.items: @@ -40,4 +44,35 @@ def populate(self): for menu in [menu for menu in menus if menu]: self.rebuild_menu(menu) -toolbar_pool.register(ForceModalDialogToolbar) + +class TitleExtensionToolbar(ExtensionToolbar): + model = None + insert_after = None + + def get_item_position(self, menu): + position = None + for items in menu._memo.values(): + for item in items: + if force_text(getattr(item, 'name', None)) in ( + force_text(self.insert_after), + '{0}...'.format(self.insert_after) + ): + position = menu._item_position(item) + 1 + break + + return position + + def populate(self): + current_page_menu = self._setup_extension_toolbar() + if not current_page_menu or not self.page: + return + + position = self.get_item_position(current_page_menu) + + urls = self.get_title_extension_admin() + for title_extension, url in urls: + current_page_menu.add_modal_item( + self.model._meta.verbose_name, + url=url, position=position, + disabled=not self.toolbar.edit_mode + ) diff --git a/barbeque/filer.py b/barbeque/filer.py index 64872ee..65cb719 100644 --- a/barbeque/filer.py +++ b/barbeque/filer.py @@ -42,6 +42,9 @@ def __init__(self, verbose_name=None, *args, **kwargs): kwargs['verbose_name'] = verbose_name if 'related_name' not in kwargs: kwargs['related_name'] = '+' + if kwargs.pop('blank', False) or kwargs.pop('null', False): + kwargs['null'] = True + kwargs['blank'] = True self.extensions = kwargs.pop('extensions', None) self.alt_text_required = kwargs.pop('alt_text_required', True) @@ -51,7 +54,7 @@ def __init__(self, verbose_name=None, *args, **kwargs): def formfield(self, **kwargs): defaults = { 'extensions': self.extensions, - 'alt_text_required': self.alt_text_required + 'alt_text_required': self.alt_text_required, } defaults.update(kwargs) return super(FilerFileField, self).formfield(**defaults) diff --git a/barbeque/files.py b/barbeque/files.py index 9e853da..169bf0d 100644 --- a/barbeque/files.py +++ b/barbeque/files.py @@ -4,12 +4,21 @@ import uuid from django.template.defaultfilters import slugify +from django.utils.deconstruct import deconstructible +from django.utils.encoding import force_text -def upload_to_path(base_path, attr=None, uuid_filename=False): - def upload_to_path_callback(instance, filename): - if attr: - parts = attr.split('__') +@deconstructible +class UploadToPath(object): + + def __init__(self, base_path, attr=None, uuid_filename=False): + self.base_path = base_path + self.attr = attr + self.uuid_filename = uuid_filename + + def __call__(self, instance, filename): + if self.attr: + parts = self.attr.split('__') obj_path = parts[:-1] field_name = parts[-1] @@ -17,22 +26,24 @@ def upload_to_path_callback(instance, filename): for part in obj_path: obj = getattr(obj, part) - path = base_path % slugify(getattr(obj, field_name, '_')) + path = self.base_path % slugify(getattr(obj, field_name, '_')) else: - path = base_path + path = self.base_path filename_parts = filename.rsplit('.', 1) - if uuid_filename: - filename = str(uuid.uuid4()) + if self.uuid_filename: + filename = force_text(uuid.uuid4()) else: filename = slugify(filename_parts[0]) - extension = len(filename_parts) > 1 and '.{0}'.format(filename_parts[-1]) or '' + extension = len(filename_parts) > 1 and u'.{0}'.format(filename_parts[-1]) or '' - return '%s%s%s' % (path, filename, extension) + return os.path.join(path, u'{0}{1}'.format(filename, extension)) - return upload_to_path_callback + +def upload_to_path(base_path, attr=None, uuid_filename=False): + return UploadToPath(base_path, attr=attr, uuid_filename=uuid_filename) class MoveableNamedTemporaryFile(object): diff --git a/barbeque/forms/__init__.py b/barbeque/forms/__init__.py new file mode 100644 index 0000000..1ab0c7e --- /dev/null +++ b/barbeque/forms/__init__.py @@ -0,0 +1,9 @@ +import warnings + +from .mixins import FloppyformsLayoutMixin, ItemLimitInlineMixin, PlaceholderFormMixin # noqa + + +warnings.warn(( + 'Importing mixins directly from barbeque.forms is deprecated and will be removed ' + 'in barbeque 1.3. Use barbeque.forms.mixins instead.' +), DeprecationWarning) diff --git a/barbeque/forms.py b/barbeque/forms/mixins.py similarity index 80% rename from barbeque/forms.py rename to barbeque/forms/mixins.py index bf0661f..036365e 100644 --- a/barbeque/forms.py +++ b/barbeque/forms/mixins.py @@ -61,3 +61,17 @@ def get_error_message(self, message, num): message = message.format(num=num, verbose_name=verbose_name) return message + + +class FloppyformsLayoutMixin(object): + row_classname = 'form-row' + div_template_name = 'modules/generic/form/layout/div.html' + + def __init__(self, *args, **kwargs): + super(FloppyformsLayoutMixin, self).__init__(*args, **kwargs) + for name, field in self.fields.items(): + widget = self.fields[name].widget + widget.widget_type = widget.__class__.__name__.lower() + + def as_div(self): + return self._render_as(self.div_template_name) diff --git a/barbeque/forms/renderer.py b/barbeque/forms/renderer.py new file mode 100644 index 0000000..c55384a --- /dev/null +++ b/barbeque/forms/renderer.py @@ -0,0 +1,39 @@ +from floppyforms.forms import LayoutRenderer + +from .mixins import FloppyformsLayoutMixin + + +class FieldsetRenderer(FloppyformsLayoutMixin, LayoutRenderer): + non_field_errors = None + + def __init__(self, form, fields=None, exclude=None, primary=False, template=None): + assert fields or exclude is not None, 'Please provide fields or exclude argument.' + + self.form = form + self.fields = fields or () + self.exclude = exclude or () + self.primary_fieldset = primary + + if template: + self.div_template_name = template + + def __str__(self): + return self.as_div() + + def hidden_fields(self): + return self.form.hidden_fields() if self.primary_fieldset else () + + def non_field_errors(self): + return self.form.non_field_errors() if self.primary_fieldset else () + + def visible_fields(self): + form_visible_fields = self.form.visible_fields() + + if self.fields: + fields = self.fields + else: + fields = [field.name for field in form_visible_fields] + + filtered_fields = [field for field in fields if field not in self.exclude] + + return [field for field in form_visible_fields if field.name in filtered_fields] diff --git a/barbeque/locale/de/LC_MESSAGES/django.mo b/barbeque/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000..291dd02 Binary files /dev/null and b/barbeque/locale/de/LC_MESSAGES/django.mo differ diff --git a/barbeque/locale/de/LC_MESSAGES/django.po b/barbeque/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..b2d93ef --- /dev/null +++ b/barbeque/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,62 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-11-28 11:54+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: barbeque/anylink.py:15 +msgid "CMS Page" +msgstr "CMS Seite" + +#: barbeque/exporter.py:119 +msgid "Export as CSV" +msgstr "Als CSV exportieren" + +#: barbeque/exporter.py:125 +msgid "Export as XLSX" +msgstr "Als XLSX exportieren" + +#: barbeque/filer.py:27 +#, python-brace-format +msgid "Invalid file extension, allowed extensions: {0}" +msgstr "Ungültige Dateiendung, erlaubte Dateitypen: {0}" + +#: barbeque/filer.py:33 +msgid "Alternative text is missing for this file." +msgstr "Bildbeschreibung fehlt für diese Datei." + +#: barbeque/forms/mixins.py:32 +#, python-brace-format +msgid "Please provide at least {num} {verbose_name}." +msgstr "Mindestens {num} {verbose_name} benötigt." + +#: barbeque/forms/mixins.py:33 +#, python-brace-format +msgid "Please provide at most {num} {verbose_name}." +msgstr "Maximal {num} {verbose_name} erlaubt." + +#: barbeque/validators.py:9 +#, python-format +msgid "You must be at least %(limit_value)d years old." +msgstr "Sie müssen mindestens %(limit_value)d Jahre alt sein." + +#: barbeque/validators.py:18 +msgid "This email address is already in use." +msgstr "Diese E-Mail-Adresse ist bereits in Verwendung." + +#: barbeque/views/mixins.py:30 +msgid "You must be logged in to access the requested page." +msgstr "Sie müssen angemeldet sein, um auf die gewünschte Seite zu gelangen" diff --git a/barbeque/templatetags/barbeque_tags.py b/barbeque/templatetags/barbeque_tags.py index b8454b2..94c6a5c 100644 --- a/barbeque/templatetags/barbeque_tags.py +++ b/barbeque/templatetags/barbeque_tags.py @@ -1,10 +1,17 @@ import re from django import template +from django.contrib.staticfiles.storage import staticfiles_storage +from django.core.exceptions import ObjectDoesNotExist from django.template.defaultfilters import stringfilter from django.utils.html import conditional_escape from django.utils.safestring import mark_safe +try: + from cms.models import Page + from cms.utils.moderator import use_draft +except ImportError: + pass register = template.Library() @@ -24,3 +31,34 @@ def set_tag(context, **kwargs): @stringfilter def starspan(value): return mark_safe(STARSPAN_RE.sub(r'\2', conditional_escape(value))) + + +@register.simple_tag +def hashed_staticfile(path): + try: + return staticfiles_storage.hashed_name(path) + except (AttributeError, ValueError): + return path + + +@register.simple_tag(takes_context=True) +def page_titleextension(context, page_id, extension): + try: + page = Page.objects.get(pk=page_id) + if 'request' in context and use_draft(context['request']): + page = page.get_draft_object() + else: + page = page.get_public_object() + except NameError: + raise ImportError( + 'django-cms is required when using page_titleextension tag') + except Page.DoesNotExist: + return None + + if not page: + return None + + try: + return getattr(page.get_title_obj(), extension) + except ObjectDoesNotExist: + return None diff --git a/barbeque/templatetags/buildcompress.py b/barbeque/templatetags/buildcompress.py index 2223f6c..63d5943 100644 --- a/barbeque/templatetags/buildcompress.py +++ b/barbeque/templatetags/buildcompress.py @@ -4,8 +4,7 @@ try: from compressor.templatetags.compress import CompressorNode, OUTPUT_FILE except ImportError: - CompressorNode = None - OUTPUT_FILE = None + pass register = template.Library() @@ -29,7 +28,11 @@ def buildcompress(parser, token): args = token.split_contents() assert len(args) == 2, 'Invalid arguments to buildcompress.' - if settings.DEBUG or not CompressorNode: + if settings.DEBUG: return BuildCompressNoopNode() - return CompressorNode(nodelist, args[1], OUTPUT_FILE, None) + try: + return CompressorNode(nodelist, args[1], OUTPUT_FILE, None) + except NameError: + raise ImportError( + 'django-compressor is required when using buildcompress tag') diff --git a/barbeque/tests/cms_urls.py b/barbeque/tests/cms_urls.py index 7751e92..77c06f3 100644 --- a/barbeque/tests/cms_urls.py +++ b/barbeque/tests/cms_urls.py @@ -1,8 +1,10 @@ from django.conf.urls import include, url from django.contrib import admin +from django.views.generic import TemplateView urlpatterns = [ url(r'^admin/', include(admin.site.urls)), + url(r'^non-cms/', TemplateView.as_view(template_name='empty_template.html')), url(r'', include('cms.urls')), ] diff --git a/barbeque/tests/resources/cmsapp/__init__.py b/barbeque/tests/resources/cmsapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/barbeque/tests/resources/cmsapp/admin.py b/barbeque/tests/resources/cmsapp/admin.py new file mode 100644 index 0000000..4e61b30 --- /dev/null +++ b/barbeque/tests/resources/cmsapp/admin.py @@ -0,0 +1,7 @@ +from cms.extensions import TitleExtensionAdmin +from django.contrib import admin + +from .models import ExtensionModel + + +admin.site.register(ExtensionModel, TitleExtensionAdmin) diff --git a/barbeque/tests/resources/cmsapp/cms_toolbars.py b/barbeque/tests/resources/cmsapp/cms_toolbars.py new file mode 100644 index 0000000..181172b --- /dev/null +++ b/barbeque/tests/resources/cmsapp/cms_toolbars.py @@ -0,0 +1,12 @@ +from cms.toolbar_pool import toolbar_pool + +from barbeque.cms.toolbar import TitleExtensionToolbar + +from .models import ExtensionModel + + +class ExtensionToolbar(TitleExtensionToolbar): + model = ExtensionModel + insert_after = 'Advanced settings' + +toolbar_pool.register(ExtensionToolbar) diff --git a/barbeque/tests/resources/cmsapp/models.py b/barbeque/tests/resources/cmsapp/models.py new file mode 100644 index 0000000..8b793fc --- /dev/null +++ b/barbeque/tests/resources/cmsapp/models.py @@ -0,0 +1,15 @@ +from cms.extensions import TitleExtension +from cms.extensions.extension_pool import extension_pool +from django.db import models + + +class ExtensionModel(TitleExtension): + name = models.CharField(max_length=255) + + class Meta: + verbose_name = 'Extension' + + def __unicode__(self): + return self.name + +extension_pool.register(ExtensionModel) diff --git a/barbeque/tests/resources/mockapp/models.py b/barbeque/tests/resources/mockapp/models.py index 34a5dd6..9afcb1f 100644 --- a/barbeque/tests/resources/mockapp/models.py +++ b/barbeque/tests/resources/mockapp/models.py @@ -13,3 +13,4 @@ class RelatedMockModel(models.Model): class DummyModel(models.Model): name = models.CharField(max_length=256) slug = models.SlugField() + email = models.EmailField(blank=True) diff --git a/barbeque/tests/settings.py b/barbeque/tests/settings.py index 4151b31..8d58d70 100644 --- a/barbeque/tests/settings.py +++ b/barbeque/tests/settings.py @@ -1,8 +1,6 @@ import os import tempfile -import django - DEBUG = True @@ -33,6 +31,7 @@ 'barbeque', 'barbeque.tests.resources.mockapp', + 'barbeque.tests.resources.cmsapp', ) ROOT_URLCONF = 'django.contrib.auth.urls' @@ -53,29 +52,18 @@ 'barbeque.anylink.CmsPageLink', ) -if django.VERSION < (1, 8): - TEMPLATE_DIRS = ( - os.path.join(os.path.dirname(__file__), 'resources', 'templates'), - ) - - TEMPLATE_CONTEXT_PROCESSORS = ( - 'django.core.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'barbeque.context_processors.settings', - ) -else: - TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(os.path.dirname(__file__), 'resources', 'templates')], - 'APP_DIRS': True, - 'OPTIONS': { - 'debug': True, - 'context_processors': [ - 'django.core.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'barbeque.context_processors.settings', - ], - }, +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(os.path.dirname(__file__), 'resources', 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'debug': True, + 'context_processors': [ + 'django.core.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'barbeque.context_processors.settings', + ], }, - ] + }, +] diff --git a/barbeque/tests/test_cms.py b/barbeque/tests/test_cms.py index 0abeeb8..de65641 100644 --- a/barbeque/tests/test_cms.py +++ b/barbeque/tests/test_cms.py @@ -1,5 +1,6 @@ +import mock from cms.api import create_page -from cms.toolbar.items import SideframeItem +from cms.toolbar.items import ModalItem, SideframeItem def test_forcemodaldialogtoolbar(admin_client, activate_cms): @@ -9,3 +10,23 @@ def test_forcemodaldialogtoolbar(admin_client, activate_cms): toolbar = response.context['request'].toolbar for item in toolbar.get_menu('admin-menu').items: assert item.__class__ is not SideframeItem + + +def test_titleextensiontoolbar_inserted(admin_client, activate_cms): + page = create_page('Test Page', 'INHERIT', 'en-us') + + response = admin_client.get('{0}?edit=on'.format(page.get_absolute_url())) + toolbar = response.context['request'].toolbar + menu = toolbar.get_menu('page') + item = menu.items[5] + assert isinstance(item, ModalItem) + assert item.name == 'Extension...' + assert item.url.startswith('/admin/cmsapp/extensionmodel/') + + +@mock.patch('barbeque.cms.toolbar.TitleExtensionToolbar.get_item_position') +def test_titleextensiontoolbar_not_inserted(position_mock, admin_client, activate_cms): + response = admin_client.get('/non-cms/') + toolbar = response.context['request'].toolbar + assert toolbar.get_menu('page') is None + assert position_mock.called is False diff --git a/barbeque/tests/test_filer.py b/barbeque/tests/test_filer.py index fa5ea81..ca703c2 100644 --- a/barbeque/tests/test_filer.py +++ b/barbeque/tests/test_filer.py @@ -72,3 +72,17 @@ class FileModel(models.Model): form_class = forms.models.modelform_factory(FileModel, fields='__all__') assert isinstance(form_class().fields['file'], AdminFileFormField) + + @pytest.mark.django_db + def test_blank_null(self): + class FileModel(models.Model): + file1 = FilerFileField(null=True) + file2 = FilerFileField(blank=True) + file3 = FilerFileField() + + assert FileModel._meta.get_field('file1').blank is True + assert FileModel._meta.get_field('file1').null is True + assert FileModel._meta.get_field('file2').blank is True + assert FileModel._meta.get_field('file2').null is True + assert FileModel._meta.get_field('file3').blank is False + assert FileModel._meta.get_field('file3').null is False diff --git a/barbeque/tests/test_forms.py b/barbeque/tests/test_forms_mixins.py similarity index 83% rename from barbeque/tests/test_forms.py rename to barbeque/tests/test_forms_mixins.py index f7b7986..e34c1fd 100644 --- a/barbeque/tests/test_forms.py +++ b/barbeque/tests/test_forms_mixins.py @@ -1,10 +1,13 @@ +import mock import pytest +import floppyforms.__future__ as floppyforms from django import forms from django.core.exceptions import ValidationError from django.forms.models import inlineformset_factory, BaseInlineFormSet -from barbeque.forms import PlaceholderFormMixin, ItemLimitInlineMixin +from barbeque.forms.mixins import ( + FloppyformsLayoutMixin, PlaceholderFormMixin, ItemLimitInlineMixin) from barbeque.tests.resources.mockapp.models import MockModel, RelatedMockModel @@ -108,3 +111,22 @@ def test_max_forms_invalid(self): assert exc.value.messages == [ 'Please provide at most 2 related mock models.'] + + +class FloppyformsLayoutForm(FloppyformsLayoutMixin, floppyforms.Form): + name = forms.CharField(max_length=255, label='Name Label') + + class Meta: + fields = '__all__' + + +class TestFloppyformsLayoutMixin: + + def test_widget_type(self): + form = FloppyformsLayoutForm() + assert form.fields['name'].widget.widget_type == 'textinput' + + @mock.patch('barbeque.tests.test_forms_mixins.FloppyformsLayoutForm._render_as') + def test_as_div(self, render_mock): + FloppyformsLayoutForm().as_div() + assert render_mock.call_args[0][0] == 'modules/generic/form/layout/div.html' diff --git a/barbeque/tests/test_forms_renderer.py b/barbeque/tests/test_forms_renderer.py new file mode 100644 index 0000000..1bd0911 --- /dev/null +++ b/barbeque/tests/test_forms_renderer.py @@ -0,0 +1,69 @@ +from datetime import date + +import mock +from django import forms + +from barbeque.forms.renderer import FieldsetRenderer + + +class FooForm(forms.Form): + name = forms.CharField(max_length=100, required=True) + email = forms.CharField(max_length=100, required=True) + birthdate = forms.DateField(required=True) + secret_field = forms.CharField(widget=forms.HiddenInput) + + +class TestRenderer: + + def get_form(self): + return FooForm({ + 'name': 'Test', + 'email': 'foo@bar.com', + 'birthdate': date(1970, 1, 1), + 'secret_field': 'Secret', + }) + + def test_form_valid(self): + form = self.get_form() + valid = form.is_valid() + + assert valid is True + + def test_hidden_fields(self): + form = self.get_form() + renderer = FieldsetRenderer(form, exclude=(), primary=True) + + assert len(renderer.hidden_fields()) == 1 + + def test_non_field_errors(self): + form = self.get_form() + renderer = FieldsetRenderer(form, exclude=(), primary=True) + + assert len(renderer.non_field_errors()) == 0 + + def test_visible_fields(self): + form = self.get_form() + renderer = FieldsetRenderer(form, exclude=(), primary=True) + + assert len(renderer.visible_fields()) == len(form.fields) - 1 + + def test_visible_fields_limited(self): + form = self.get_form() + renderer = FieldsetRenderer(form, fields=('name',), primary=True) + + assert len(renderer.visible_fields()) == 1 + + def test_template(self): + form = self.get_form() + renderer = FieldsetRenderer(form, exclude=(), primary=True) + assert renderer.div_template_name == 'modules/generic/form/layout/div.html' + + renderer = FieldsetRenderer(form, exclude=(), primary=True, template='foo.html') + assert renderer.div_template_name == 'foo.html' + + @mock.patch('barbeque.forms.renderer.FieldsetRenderer.as_div') + def test_str(self, div_mock): + form = self.get_form() + renderer = FieldsetRenderer(form, exclude=(), primary=True) + renderer.__str__() + assert div_mock.called is True diff --git a/barbeque/tests/test_static_files.py b/barbeque/tests/test_static_files.py deleted file mode 100644 index 59818e2..0000000 --- a/barbeque/tests/test_static_files.py +++ /dev/null @@ -1,200 +0,0 @@ -import os -import mock -from collections import OrderedDict - -import pytest -from django.http import HttpResponse, HttpResponseNotFound, HttpResponsePermanentRedirect -from django.shortcuts import redirect -from django.conf.urls import url - -from barbeque.staticfiles.middleware import ServeStaticFileMiddleware - - -def foo_view(request): - return redirect('bar', permanent=True) - - -def bar_view(request): - return HttpResponse('Hllo FooBar!') - - -urlpatterns = [ - url(r'^bar$', bar_view, name='bar'), - url(r'^foo$', foo_view, name='foo'), - -] - - -class TestServeStaticFileMiddleware: - - @pytest.fixture(autouse=True) - def setup(self, settings): - settings.ROOT_DIR = os.path.dirname(os.path.dirname(__file__)) - settings.STATIC_ROOT = os.path.join(settings.ROOT_DIR, 'tests', 'resources', 'static') - # settings.STATICFILES_STORAGE = ( - # 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage') - settings.STATICFILES_STORAGE = ( - 'barbeque.staticfiles.storage.CompactManifestStaticFilesStorage') - - @pytest.fixture - def patch_settings(self, settings): - """ - Patch settings for tests fith django client - """ - settings.STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - 'compressor.finders.CompressorFinder', - ) - settings.MIDDLEWARE_CLASSES = [ - 'barbeque.staticfiles.middleware.ServeStaticFileMiddleware', - ] - settings.INSTALLED_APPS = settings.INSTALLED_APPS + ('django.contrib.staticfiles',) - settings.ROOT_URLCONF = 'barbeque.tests.test_static_files' - - def test_file_exists(self, rf): - request = rf.get('/static/test.jpg') - middleware = ServeStaticFileMiddleware() - response = middleware.process_response(request, HttpResponseNotFound('')) - assert response.status_code == 200 - assert response['Content-Type'] == 'image/jpeg' - assert len(response.items()) == 3 - assert response.has_header('Content-Length') - assert response.has_header('Last-Modified') - - def test_file_missing(self, rf): - request = rf.get('/static/doesnotexist.jpg') - middleware = ServeStaticFileMiddleware() - response = middleware.process_response(request, HttpResponseNotFound('')) - assert response.status_code == 404 - - def test_unknown_prefix(self, rf): - request = rf.get('/foo/test.jpg') - middleware = ServeStaticFileMiddleware() - response = middleware.process_response(request, HttpResponseNotFound('')) - assert response.status_code == 404 - - def test_redirect_for_static(self, rf): - request = rf.get('/static/test.jpg') - middleware = ServeStaticFileMiddleware() - response = middleware.process_response( - request, HttpResponsePermanentRedirect('/static/test.jpg/')) - assert response.status_code == 200 - - def test_redirect_other(self, rf): - request = rf.get('/foo') - middleware = ServeStaticFileMiddleware() - redirect = HttpResponsePermanentRedirect('/foo/') - response = middleware.process_response(request, redirect) - assert response == redirect - - @mock.patch('barbeque.staticfiles.middleware.ServeStaticFileMiddleware.process_response') - def test_new_style_middleware(self, process_response_mock, rf): - request = rf.get('/static/test.jpg') - get_response_mock = mock.Mock() - get_response_mock.return_value = HttpResponseNotFound() - middleware = ServeStaticFileMiddleware(get_response=get_response_mock) - middleware(request) - get_response_mock.assert_called_with(request) - process_response_mock.assert_called_with( - request, get_response_mock.return_value) - - def test_with_client_hit(self, client, patch_settings): - response = client.get('/static/test.jpg') - assert response.status_code == 200 - - def test_with_client_redirect(self, client, patch_settings): - response = client.get('/foo') - assert response.status_code == 301 - assert response['Location'].endswith('/bar') - - def test_with_client_query_params(self, client, patch_settings): - response = client.get('/static/test.jpg?v=1') - assert response.status_code == 200 - assert response['Content-Type'] == 'image/jpeg' - - -class TestServeStaticFileMiddlewareWithHashedFiles: - - @pytest.fixture(autouse=True) - def setup(self, settings): - settings.ROOT_DIR = os.path.dirname(os.path.dirname(__file__)) - settings.STATIC_ROOT = os.path.join(settings.ROOT_DIR, 'tests', 'resources', 'static') - settings.STATICFILES_STORAGE = ( - 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage') - - @pytest.fixture - def patch_settings(self, settings): - """ - Patch settings for tests fith django client - """ - settings.STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - 'compressor.finders.CompressorFinder', - ) - settings.MIDDLEWARE_CLASSES = [ - 'barbeque.staticfiles.middleware.ServeStaticFileMiddleware', - ] - settings.INSTALLED_APPS = settings.INSTALLED_APPS + ('django.contrib.staticfiles',) - settings.ROOT_URLCONF = 'barbeque.tests.test_static_files' - - def test_unhash_file_name(self): - middleware = ServeStaticFileMiddleware() - assert middleware.unhash_file_name( - '/static/test_hash.11aa22bb33cc.jpg') == ('/static/test_hash.jpg') - assert middleware.unhash_file_name('test_hash.jpg') == 'test_hash.jpg' - assert middleware.unhash_file_name( - 'test_hash.11aa22bb33cc.11aa22bb33cc.jpg') == ('test_hash.11aa22bb33cc.jpg') - assert middleware.unhash_file_name('test_hash.11aa22bb33cc') == 'test_hash' - assert middleware.unhash_file_name('11aa22bb33cc') == '11aa22bb33cc' - assert middleware.unhash_file_name('11aa22bb33cc.jpg') == '11aa22bb33cc.jpg' - assert middleware.unhash_file_name('.11aa22bb33cc.jpg') == '.11aa22bb33cc.jpg' - - def test_hash_file_exists(self, rf): - request = rf.get('/static/test_hash.11aa22bb33cc.jpg') - middleware = ServeStaticFileMiddleware() - response = middleware.process_response(request, HttpResponseNotFound('')) - assert response.status_code == 200 - assert response['Content-Type'] == 'image/jpeg' - assert len(response.items()) == 3 - assert response.has_header('Content-Length') - assert response.has_header('Last-Modified') - - def test_hash_file_original_exists(self, rf): - request = rf.get('/static/test_hash.jpg') - middleware = ServeStaticFileMiddleware() - response = middleware.process_response(request, HttpResponseNotFound('')) - assert response.status_code == 200 - assert response['Content-Type'] == 'image/jpeg' - assert len(response.items()) == 3 - assert response.has_header('Content-Length') - assert response.has_header('Last-Modified') - - def test_old_hash(self, rf): - request = rf.get('/static/test_hash.44dd55ee66ff.jpg') - middleware = ServeStaticFileMiddleware() - response = middleware.process_response(request, HttpResponseNotFound('')) - assert len(response.items()) == 3 - assert response.has_header('Content-Length') - assert response.has_header('Last-Modified') - - def test_hash_file_exists_with_client_hit(self, client, patch_settings): - response = client.get('/static/test_hash.11aa22bb33cc.jpg') - assert response.status_code == 200 - - def test_hash_file_original_exists_with_client_hit(self, client, patch_settings): - response = client.get('/static/test_hash.jpg') - assert response.status_code == 200 - - def test_hash_old_hash_with_client_hit(self, client, patch_settings): - response = client.get('/static/test_hash.44dd55ee66ff.jpg') - assert response.status_code == 200 - - @mock.patch('django.contrib.staticfiles.storage.ManifestStaticFilesStorage.load_manifest') - def test_no_staticfiles_manifest(self, manifest_mock, rf): - manifest_mock.return_value = OrderedDict() - request = rf.get('/static/test_hash.jpg') - middleware = ServeStaticFileMiddleware() - response = middleware.process_response(request, HttpResponseNotFound('')) - assert response.status_code == 404 diff --git a/barbeque/tests/test_staticfiles.py b/barbeque/tests/test_staticfiles.py index 59818e2..ed2eb04 100644 --- a/barbeque/tests/test_staticfiles.py +++ b/barbeque/tests/test_staticfiles.py @@ -50,7 +50,7 @@ def patch_settings(self, settings): 'barbeque.staticfiles.middleware.ServeStaticFileMiddleware', ] settings.INSTALLED_APPS = settings.INSTALLED_APPS + ('django.contrib.staticfiles',) - settings.ROOT_URLCONF = 'barbeque.tests.test_static_files' + settings.ROOT_URLCONF = 'barbeque.tests.test_staticfiles' def test_file_exists(self, rf): request = rf.get('/static/test.jpg') @@ -68,6 +68,12 @@ def test_file_missing(self, rf): response = middleware.process_response(request, HttpResponseNotFound('')) assert response.status_code == 404 + def test_static_folder(self, rf): + request = rf.get('/static/doesnotexist/') + middleware = ServeStaticFileMiddleware() + response = middleware.process_response(request, HttpResponseNotFound('')) + assert response.status_code == 404 + def test_unknown_prefix(self, rf): request = rf.get('/foo/test.jpg') middleware = ServeStaticFileMiddleware() @@ -137,7 +143,7 @@ def patch_settings(self, settings): 'barbeque.staticfiles.middleware.ServeStaticFileMiddleware', ] settings.INSTALLED_APPS = settings.INSTALLED_APPS + ('django.contrib.staticfiles',) - settings.ROOT_URLCONF = 'barbeque.tests.test_static_files' + settings.ROOT_URLCONF = 'barbeque.tests.test_staticfiles' def test_unhash_file_name(self): middleware = ServeStaticFileMiddleware() diff --git a/barbeque/tests/test_templatetags.py b/barbeque/tests/test_templatetags.py index 071ebef..8da9789 100644 --- a/barbeque/tests/test_templatetags.py +++ b/barbeque/tests/test_templatetags.py @@ -1,7 +1,11 @@ import mock +import pytest +from cms.api import create_page, publish_page +from django.contrib.auth.models import User from django.template import Context, Node, Template from barbeque.templatetags.barbeque_tags import starspan +from barbeque.tests.resources.cmsapp.models import ExtensionModel class TestTemplateTags: @@ -48,3 +52,89 @@ def test_buildcompress_tag_no_debug(self, node_mock, settings): template.render(Context()) assert node_mock.called is True + + +@pytest.mark.django_db +class TestPageTitleExtensionTemplateTag: + + @mock.patch('barbeque.templatetags.barbeque_tags.Page.objects.get') + def test_no_cms(self, page_mock, activate_cms, rf): + page_mock.side_effect = NameError + template = Template( + '{% load barbeque_tags %}{% page_titleextension 1 "extensionmodel" %}') + context = Context({'request': rf.get('/')}) + with pytest.raises(ImportError): + assert template.render(context) == '' + + def test_page_not_found(self, activate_cms, rf): + template = Template( + '{% load barbeque_tags %}{% page_titleextension 1 "extensionmodel" %}') + context = Context({'request': rf.get('/')}) + assert template.render(context) == 'None' + + def test_no_page(self, activate_cms, rf): + request = rf.get('/') + request.user = User() + page = create_page('Test Page', 'INHERIT', 'en-us') + template = Template(( + '{%% load barbeque_tags %%}' + '{%% page_titleextension %s "extensionmodel" %%}' + ) % page.pk) + context = Context({'request': request}) + assert template.render(context) == 'None' + + def test_extension_not_found(self, activate_cms, rf): + request = rf.get('/') + request.user = User.objects.create(username='admin', is_superuser=True) + + page = create_page('Test Page', 'INHERIT', 'en-us') + publish_page(page, request.user, 'en-us') + page.refresh_from_db() + + template = Template(( + '{%% load barbeque_tags %%}' + '{%% page_titleextension %s "extensionmodel" %%}' + ) % page.pk) + context = Context({'request': request}) + assert template.render(context) == 'None' + + def test_extension_found_public(self, activate_cms, rf): + request = rf.get('/') + request.user = User.objects.create(username='admin', is_superuser=True) + + page = create_page('Test Page', 'INHERIT', 'en-us') + publish_page(page, request.user, 'en-us') + page.refresh_from_db() + + ExtensionModel.objects.create( + extended_object=page.get_public_object().get_title_obj(), name='public') + ExtensionModel.objects.create( + extended_object=page.get_draft_object().get_title_obj(), name='draft') + + template = Template(( + '{%% load barbeque_tags %%}' + '{%% page_titleextension %s "extensionmodel" %%}' + ) % page.pk) + context = Context({'request': request}) + assert template.render(context) == 'public' + + def test_extension_found_draft(self, activate_cms, rf): + request = rf.get('/') + request.user = User.objects.create(username='admin', is_staff=True, is_superuser=True) + request.session = {'cms_edit': True} + + page = create_page('Test Page', 'INHERIT', 'en-us') + publish_page(page, request.user, 'en-us') + page.refresh_from_db() + + ExtensionModel.objects.create( + extended_object=page.get_public_object().get_title_obj(), name='public') + ExtensionModel.objects.create( + extended_object=page.get_draft_object().get_title_obj(), name='draft') + + template = Template(( + '{%% load barbeque_tags %%}' + '{%% page_titleextension %s "extensionmodel" %%}' + ) % page.pk) + context = Context({'request': request}) + assert template.render(context) == 'draft' diff --git a/barbeque/tests/test_validators.py b/barbeque/tests/test_validators.py new file mode 100644 index 0000000..e249eae --- /dev/null +++ b/barbeque/tests/test_validators.py @@ -0,0 +1,40 @@ +from datetime import date +from dateutil.relativedelta import relativedelta + +import pytest +from django.core.exceptions import ValidationError + +from barbeque.validators import AgeValidator, UniqueEmailValidator +from barbeque.tests.resources.mockapp.models import DummyModel + + +class TestAgeValidator: + def setup(self): + self.validator = AgeValidator(10) + + def test_invalid(self): + with pytest.raises(ValidationError): + self.validator( + date.today() - relativedelta(years=10) + relativedelta(days=1)) + + def test_valid(self): + self.validator(date.today() - relativedelta(years=10)) + + +@pytest.mark.django_db +class TestEmailValidator: + + def test_unique(self): + UniqueEmailValidator(DummyModel.objects.all())('foo@bar.baz') + + def test_not_unique(self): + DummyModel.objects.create(name='foo', slug='bar', email='foo@bar.baz') + + with pytest.raises(ValidationError): + UniqueEmailValidator(DummyModel.objects.all())('foo@bar.baz') + + def test_not_unique_uppercase(self): + DummyModel.objects.create(name='foo', slug='bar', email='foo@bar.baz') + + with pytest.raises(ValidationError): + UniqueEmailValidator(DummyModel.objects.all())('FOO@BAR.baz') diff --git a/barbeque/validators.py b/barbeque/validators.py new file mode 100644 index 0000000..285b105 --- /dev/null +++ b/barbeque/validators.py @@ -0,0 +1,22 @@ +from dateutil.relativedelta import relativedelta + +from django.core.validators import BaseValidator +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + + +class AgeValidator(BaseValidator): + message = _(u'You must be at least %(limit_value)d years old.') + code = 'age' + + def compare(self, value, min_age): + today = timezone.now() + return value > (today - relativedelta(years=min_age)).date() + + +class UniqueEmailValidator(BaseValidator): + message = _('This email address is already in use.') + code = 'unique_email' + + def compare(self, value, qset): + return qset.filter(email__iexact=value).exists() diff --git a/conftest.py b/conftest.py index be5240d..62b6dcf 100644 --- a/conftest.py +++ b/conftest.py @@ -1,7 +1,6 @@ import shutil import tempfile -import django import pytest @@ -18,22 +17,19 @@ def activate_cms(settings): settings.ROOT_URLCONF = 'barbeque.tests.cms_urls' settings.MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'cms.middleware.toolbar.ToolbarMiddleware', 'cms.middleware.page.CurrentPageMiddleware', ) - if django.VERSION[:2] >= (1, 7): - settings.MIDDLEWARE_CLASSES += ( - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - ) - settings.CMS_TEMPLATES = (('empty_template.html', 'empty'),) settings.CMS_TOOLBARS = [ 'cms.cms_toolbars.PlaceholderToolbar', 'cms.cms_toolbars.BasicToolbar', 'cms.cms_toolbars.PageToolbar', 'barbeque.cms.toolbar.ForceModalDialogToolbar', + 'barbeque.tests.resources.cmsapp.cms_toolbars.ExtensionToolbar', ] yield diff --git a/pytest.ini b/pytest.ini index 15b1c6c..119ee56 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,5 @@ [pytest] -addopts = -vs --cache-clear --tb=short --pep8 --flakes -p no:doctest +addopts = -vs --cache-clear --tb=short --pep8 --flakes -p no:doctest --no-migrations norecursedirs = .tox build docs python_files = diff --git a/setup.py b/setup.py index df4189e..aea25f0 100644 --- a/setup.py +++ b/setup.py @@ -9,6 +9,24 @@ version = '1.1.1' +# TEMPORARY FIX FOR +# https://bitbucket.org/pypa/setuptools/issues/450/egg_info-command-is-very-slow-if-there-are +TO_OMIT = ['.git', '.tox'] +orig_os_walk = os.walk + + +def patched_os_walk(path, *args, **kwargs): + for (dirpath, dirnames, filenames) in orig_os_walk(path, *args, **kwargs): + if '.git' in dirnames: + # We're probably in our own root directory. + print("MONKEY PATCH: omitting a few directories like .git and .tox...") + dirnames[:] = list(set(dirnames) - set(TO_OMIT)) + yield (dirpath, dirnames, filenames) + +os.walk = patched_os_walk +# END IF TEMPORARY FIX. + + if sys.argv[-1] == 'publish': os.system('python setup.py sdist upload') os.system('python setup.py bdist_wheel upload') @@ -22,19 +40,22 @@ 'mock==1.3.0', 'openpyxl==2.2.6', 'psutil==3.2.1', + 'python-dateutil==2.4.2', 'pytest==3.0.3', - 'pytest-cov==2.1.0', + 'pytest-cov==2.3.1', 'pytest-pep8==1.0.6', 'pytest-flakes==1.0.1', 'pytest-django==3.0.0', - 'factory-boy==2.5.2', + 'pytest-isort==0.1.0', + 'factory-boy==2.7.0', 'Pillow==3.4.0', 'django-anylink==0.3.0', 'django-treebeard>=4.0', - 'django-cms==3.2.5', + 'django-cms==3.3.3', 'django-polymorphic==0.8.1', 'django-compressor==1.6', 'django-filer==1.1.1', + 'django-floppyforms==1.7.0', 'tox==2.3.1', 'tox-pyenv==1.0.3', ]