From 254c4a2658894c8c993a3921bec2580a6138b49b Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Thu, 9 Jan 2020 16:40:35 +0100 Subject: [PATCH] Reproducible lint: Invoke `pre-commit run --all-files` on full repo. --- docs/conf.py | 77 +- setup.py | 78 +- src/wiki/__init__.py | 4 +- src/wiki/admin.py | 45 +- src/wiki/apps.py | 17 +- src/wiki/checks.py | 58 +- src/wiki/conf/settings.py | 224 +++--- src/wiki/core/exceptions.py | 2 +- src/wiki/core/http.py | 13 +- src/wiki/core/markdown/__init__.py | 20 +- src/wiki/core/markdown/mdx/codehilite.py | 46 +- src/wiki/core/markdown/mdx/previewlinks.py | 9 +- src/wiki/core/markdown/mdx/responsivetable.py | 14 +- src/wiki/core/paginator.py | 7 +- src/wiki/core/permissions.py | 23 +- src/wiki/core/plugins/base.py | 15 +- src/wiki/core/plugins/loader.py | 2 +- src/wiki/core/plugins/registry.py | 28 +- src/wiki/core/utils.py | 2 +- src/wiki/core/version.py | 31 +- src/wiki/decorators.py | 83 +- src/wiki/editors/base.py | 2 +- src/wiki/editors/markitup.py | 38 +- src/wiki/forms.py | 440 ++++++----- src/wiki/forms_account_handling.py | 41 +- src/wiki/managers.py | 59 +- src/wiki/migrations/0001_initial.py | 475 ++++++++--- src/wiki/migrations/0002_urlpath_moved_to.py | 16 +- src/wiki/models/__init__.py | 14 +- src/wiki/models/article.py | 197 ++--- src/wiki/models/pluginbase.py | 100 +-- src/wiki/models/urlpath.py | 186 ++--- src/wiki/plugins/attachments/__init__.py | 2 +- src/wiki/plugins/attachments/admin.py | 2 +- src/wiki/plugins/attachments/apps.py | 4 +- src/wiki/plugins/attachments/forms.py | 82 +- .../attachments/markdown_extensions.py | 51 +- .../attachments/migrations/0001_initial.py | 129 ++- .../migrations/0002_auto_20151118_1816.py | 8 +- src/wiki/plugins/attachments/models.py | 103 +-- src/wiki/plugins/attachments/settings.py | 34 +- src/wiki/plugins/attachments/urls.py | 52 +- src/wiki/plugins/attachments/views.py | 268 ++++--- src/wiki/plugins/attachments/wiki_plugin.py | 30 +- src/wiki/plugins/editsection/__init__.py | 2 +- src/wiki/plugins/editsection/apps.py | 4 +- .../editsection/markdown_extensions.py | 49 +- src/wiki/plugins/editsection/settings.py | 6 +- src/wiki/plugins/editsection/views.py | 89 ++- src/wiki/plugins/editsection/wiki_plugin.py | 14 +- src/wiki/plugins/globalhistory/__init__.py | 2 +- src/wiki/plugins/globalhistory/apps.py | 4 +- src/wiki/plugins/globalhistory/settings.py | 2 +- src/wiki/plugins/globalhistory/views.py | 22 +- src/wiki/plugins/globalhistory/wiki_plugin.py | 14 +- src/wiki/plugins/help/__init__.py | 2 +- src/wiki/plugins/help/apps.py | 4 +- src/wiki/plugins/help/wiki_plugin.py | 14 +- src/wiki/plugins/images/__init__.py | 2 +- src/wiki/plugins/images/admin.py | 13 +- src/wiki/plugins/images/apps.py | 9 +- src/wiki/plugins/images/checks.py | 4 +- src/wiki/plugins/images/forms.py | 32 +- .../plugins/images/markdown_extensions.py | 22 +- .../plugins/images/migrations/0001_initial.py | 61 +- .../migrations/0002_auto_20151118_1811.py | 10 +- src/wiki/plugins/images/models.py | 49 +- src/wiki/plugins/images/settings.py | 37 +- .../images/templatetags/wiki_images_tags.py | 4 +- src/wiki/plugins/images/views.py | 116 +-- src/wiki/plugins/images/wiki_plugin.py | 87 +- src/wiki/plugins/links/__init__.py | 2 +- src/wiki/plugins/links/apps.py | 4 +- src/wiki/plugins/links/mdx/djangowikilinks.py | 59 +- src/wiki/plugins/links/mdx/urlize.py | 79 +- src/wiki/plugins/links/settings.py | 2 +- src/wiki/plugins/links/views.py | 22 +- src/wiki/plugins/links/wiki_plugin.py | 38 +- src/wiki/plugins/macros/__init__.py | 2 +- src/wiki/plugins/macros/apps.py | 4 +- src/wiki/plugins/macros/mdx/macro.py | 54 +- src/wiki/plugins/macros/mdx/toc.py | 5 +- src/wiki/plugins/macros/mdx/wikilinks.py | 30 +- src/wiki/plugins/macros/settings.py | 11 +- .../macros/templatetags/wiki_macro_tags.py | 7 +- src/wiki/plugins/macros/wiki_plugin.py | 18 +- src/wiki/plugins/notifications/__init__.py | 2 +- src/wiki/plugins/notifications/apps.py | 30 +- src/wiki/plugins/notifications/forms.py | 138 ++-- .../wiki_notifications_create_defaults.py | 41 +- .../notifications/migrations/0001_initial.py | 34 +- .../migrations/0002_auto_20151118_1811.py | 5 +- src/wiki/plugins/notifications/models.py | 42 +- src/wiki/plugins/notifications/settings.py | 3 +- src/wiki/plugins/notifications/views.py | 44 +- src/wiki/plugins/notifications/wiki_plugin.py | 14 +- src/wiki/plugins/redlinks/__init__.py | 2 +- src/wiki/plugins/redlinks/apps.py | 4 +- src/wiki/plugins/redlinks/mdx/redlinks.py | 2 +- src/wiki/plugins/redlinks/wiki_plugin.py | 2 +- src/wiki/sites.py | 232 ++++-- .../wiki/bootstrap/css/wiki-bootstrap.min.css | 2 +- src/wiki/templatetags/wiki_tags.py | 60 +- src/wiki/urls.py | 261 +++--- src/wiki/views/accounts.py | 37 +- src/wiki/views/article.py | 455 ++++++----- src/wiki/views/deleted_list.py | 4 +- src/wiki/views/mixins.py | 27 +- testproject/testproject/settings/base.py | 102 ++- .../testproject/settings/codehilite.py | 9 +- .../testproject/settings/customauthuser.py | 10 +- testproject/testproject/settings/dev.py | 13 +- testproject/testproject/settings/sendfile.py | 4 +- testproject/testproject/urls.py | 18 +- testproject/testproject/views.py | 30 +- tests/base.py | 28 +- tests/core/test_accounts.py | 75 +- tests/core/test_basic.py | 20 +- tests/core/test_checks.py | 55 +- tests/core/test_commands.py | 8 +- tests/core/test_forms.py | 16 +- tests/core/test_managers.py | 51 +- tests/core/test_markdown.py | 115 ++- tests/core/test_models.py | 43 +- tests/core/test_sites.py | 63 +- tests/core/test_template_filters.py | 194 ++--- tests/core/test_template_tags.py | 157 ++-- tests/core/test_urls.py | 36 +- tests/core/test_utils.py | 1 - tests/core/test_views.py | 740 +++++++++--------- tests/plugins/attachments/test_commands.py | 11 +- tests/plugins/attachments/test_models.py | 12 +- tests/plugins/attachments/test_views.py | 93 +-- tests/plugins/editsection/test_editsection.py | 99 +-- .../globalhistory/test_globalhistory.py | 80 +- tests/plugins/images/test_forms.py | 6 +- tests/plugins/images/test_views.py | 142 ++-- tests/plugins/links/test_links.py | 31 +- tests/plugins/links/test_urlize.py | 272 +++---- tests/plugins/macros/test_links.py | 5 +- tests/plugins/macros/test_toc.py | 18 +- tests/plugins/notifications/test_forms.py | 1 - tests/plugins/notifications/test_views.py | 53 +- tests/settings.py | 76 +- tests/testdata/migrations/0001_initial.py | 169 +++- tests/testdata/models.py | 2 +- tests/testdata/urls.py | 19 +- 147 files changed, 4607 insertions(+), 3853 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index c9a14e24..bbe68984 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,8 +37,8 @@ import django # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../src')) -sys.path.insert(0, os.path.abspath('../testproject')) +sys.path.insert(0, os.path.abspath("../src")) +sys.path.insert(0, os.path.abspath("../testproject")) # -- General configuration ------------------------------------------------ @@ -62,7 +62,7 @@ def process_docstring(app, what, name, obj, options, lines): for field in fields: # Skip ManyToOneRel and ManyToManyRel fields which have no 'verbose_name' or 'help_text' - if not hasattr(field, 'verbose_name'): + if not hasattr(field, "verbose_name"): continue # Decode and strip any html out of the field's help text @@ -75,62 +75,65 @@ def process_docstring(app, what, name, obj, options, lines): if help_text: # Add the model field to the end of the docstring as a param # using the help text as the description - lines.append(u':param %s: %s' % (field.attname, help_text)) + lines.append(u":param %s: %s" % (field.attname, help_text)) else: # Add the model field to the end of the docstring as a param # using the verbose name as the description - lines.append(u':param %s: %s' % (field.attname, verbose_name)) + lines.append(u":param %s: %s" % (field.attname, verbose_name)) # Add the field's type to the docstring if isinstance(field, models.ForeignKey): for to in field.to_fields: - lines.append(u':type %s: %s to :class:`~%s`' % (field.attname, type(field).__name__, to)) + lines.append( + u":type %s: %s to :class:`~%s`" + % (field.attname, type(field).__name__, to) + ) else: - lines.append(u':type %s: %s' % (field.attname, type(field).__name__)) + lines.append(u":type %s: %s" % (field.attname, type(field).__name__)) return lines extlinks = { - 'url-issue': ('https://github.com/django-wiki/django-wiki/issues/%s', '#'), + "url-issue": ("https://github.com/django-wiki/django-wiki/issues/%s", "#"), } def setup(app): # Register the docstring processor with sphinx - app.connect('autodoc-process-docstring', process_docstring) + app.connect("autodoc-process-docstring", process_docstring) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.extlinks', - 'sphinx.ext.todo', - 'sphinx.ext.viewcode', + "sphinx.ext.autodoc", + "sphinx.ext.extlinks", + "sphinx.ext.todo", + "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'django-wiki' -copyright = '{}, Benjamin Bach'.format(datetime.now().year) # noqa +project = "django-wiki" +copyright = "{}, Benjamin Bach".format(datetime.now().year) # noqa path = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) sys.path = [path] + sys.path -sys.path = [os.path.join(path, 'wiki')] + sys.path +sys.path = [os.path.join(path, "wiki")] + sys.path import wiki # noqa @@ -157,7 +160,7 @@ release = wiki.__version__ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None @@ -174,25 +177,25 @@ exclude_patterns = ['_build'] # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] linkcheck_ignore = [ - r'wiki.+', + r"wiki.+", ] # -- Options for HTML output --------------------------------------------------- -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +on_rtd = os.environ.get("READTHEDOCS", None) == "True" if on_rtd: os.system("sphinx-apidoc --doc-project='Python Reference' -f -o . ../wiki") if on_rtd: - html_theme = 'default' + html_theme = "default" else: - html_theme = 'sphinx_rtd_theme' + html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -265,7 +268,7 @@ html_static_path = [] # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'django-wikidoc' +htmlhelp_basename = "django-wikidoc" # -- Options for LaTeX output -------------------------------------------------- @@ -273,10 +276,8 @@ htmlhelp_basename = 'django-wikidoc' latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # 'preamble': '', } @@ -285,8 +286,11 @@ latex_elements = { # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( - 'index', 'django-wiki.tex', 'django-wiki Documentation', - 'Benjamin Bach', 'manual' + "index", + "django-wiki.tex", + "django-wiki Documentation", + "Benjamin Bach", + "manual", ), ] @@ -316,8 +320,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'django-wiki', 'django-wiki Documentation', - ['Benjamin Bach'], 1) + ("index", "django-wiki", "django-wiki Documentation", ["Benjamin Bach"], 1) ] # If true, show URL addresses after external links. @@ -331,9 +334,13 @@ man_pages = [ # dir menu entry, description, category) texinfo_documents = [ ( - 'index', 'django-wiki', 'django-wiki Documentation', - 'Benjamin Bach', 'django-wiki', 'Wiki engine for Django - with real data models!', - 'Miscellaneous' + "index", + "django-wiki", + "django-wiki Documentation", + "Benjamin Bach", + "django-wiki", + "Wiki engine for Django - with real data models!", + "Miscellaneous", ), ] diff --git a/setup.py b/setup.py index 4bfd36b8..2847fa9f 100755 --- a/setup.py +++ b/setup.py @@ -7,9 +7,7 @@ from glob import glob from setuptools import find_packages, setup -sys.path.append( - os.path.join(os.path.dirname(__file__), 'src') -) +sys.path.append(os.path.join(os.path.dirname(__file__), "src")) # noqa from wiki import __version__ # isort:skip # noqa @@ -31,34 +29,34 @@ install_requirements = [ "django-mptt>=0.9,<0.10", "django-sekizai>=0.10", "sorl-thumbnail>=12,<13", - "Markdown>=3.1,<3.2.0" + "Markdown>=3.1,<3.2.0", ] test_requirements = [ - 'django-functest>=1.0.3,<1.1', - 'pytest>=5.3,<5.4', - 'pytest-django', - 'pytest-cov', - 'pytest-pythonpath', + "django-functest>=1.0.3,<1.1", + "pytest>=5.3,<5.4", + "pytest-django", + "pytest-cov", + "pytest-pythonpath", ] test_lint_requirements = [ - 'flake8>=3.7,<3.8', - 'flake8-isort', + "flake8>=3.7,<3.8", + "flake8-isort", ] setup_requirements = [ - 'pytest-runner', + "pytest-runner", ] -development_requirements = test_requirements + test_lint_requirements + [ - 'pre-commit', -] +development_requirements = ( + test_requirements + test_lint_requirements + ["pre-commit",] +) extras_requirements = { - 'devel': development_requirements, - 'test': test_requirements, - 'testlint': test_lint_requirements, + "devel": development_requirements, + "test": test_requirements, + "testlint": test_lint_requirements, } setup( @@ -70,30 +68,32 @@ setup( description="A wiki system written for the Django framework.", license="GPLv3", keywords=["django", "wiki", "markdown"], - packages=find_packages('src'), - package_dir={'': 'src'}, - py_modules=[os.path.splitext(os.path.basename(path))[0] for path in glob('src/*.py')], - long_description=open('README.rst').read(), + packages=find_packages("src"), + package_dir={"": "src"}, + py_modules=[ + os.path.splitext(os.path.basename(path))[0] for path in glob("src/*.py") + ], + long_description=open("README.rst").read(), zip_safe=False, install_requires=install_requirements, classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - 'Topic :: Software Development', - 'Topic :: Software Development :: Libraries :: Application Frameworks', + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Environment :: Web Environment", + "Framework :: Django", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries :: Application Frameworks", ], include_package_data=True, setup_requires=setup_requirements, diff --git a/src/wiki/__init__.py b/src/wiki/__init__.py index 17cf06bb..ba5dca07 100644 --- a/src/wiki/__init__.py +++ b/src/wiki/__init__.py @@ -17,7 +17,7 @@ from wiki.core.version import get_version -default_app_config = 'wiki.apps.WikiConfig' +default_app_config = "wiki.apps.WikiConfig" -VERSION = (0, 6, 0, 'alpha', 0) +VERSION = (0, 6, 0, "alpha", 0) __version__ = get_version(VERSION) diff --git a/src/wiki/admin.py b/src/wiki/admin.py index 9a3ef22d..8143bd6f 100644 --- a/src/wiki/admin.py +++ b/src/wiki/admin.py @@ -11,11 +11,10 @@ class ArticleObjectAdmin(GenericTabularInline): model = models.ArticleForObject extra = 1 max_num = 1 - raw_id_fields = ('article',) + raw_id_fields = ("article",) class ArticleRevisionForm(forms.ModelForm): - class Meta: model = models.ArticleRevision exclude = () @@ -24,12 +23,12 @@ class ArticleRevisionForm(forms.ModelForm): super().__init__(*args, **kwargs) # TODO: This pattern is too weird editor = editors.getEditor() - self.fields['content'].widget = editor.get_admin_widget() + self.fields["content"].widget = editor.get_admin_widget() class ArticleRevisionAdmin(admin.ModelAdmin): form = ArticleRevisionForm - list_display = ('title', 'created', 'modified', 'user', 'ip_address') + list_display = ("title", "created", "modified", "user", "ip_address") class Media: js = editors.getEditorClass().AdminMedia.js @@ -39,9 +38,14 @@ class ArticleRevisionAdmin(admin.ModelAdmin): class ArticleRevisionInline(admin.TabularInline): model = models.ArticleRevision form = ArticleRevisionForm - fk_name = 'article' + fk_name = "article" extra = 1 - fields = ('content', 'title', 'deleted', 'locked',) + fields = ( + "content", + "title", + "deleted", + "locked", + ) class Media: js = editors.getEditorClass().AdminMedia.js @@ -49,7 +53,6 @@ class ArticleRevisionInline(admin.TabularInline): class ArticleForm(forms.ModelForm): - class Meta: model = models.Article exclude = () @@ -57,32 +60,36 @@ class ArticleForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance.pk: - revisions = models.ArticleRevision.objects.filter( - article=self.instance) - self.fields['current_revision'].queryset = revisions + revisions = models.ArticleRevision.objects.filter(article=self.instance) + self.fields["current_revision"].queryset = revisions else: self.fields[ - 'current_revision'].queryset = models.ArticleRevision.objects.none() - self.fields['current_revision'].widget = forms.HiddenInput() + "current_revision" + ].queryset = models.ArticleRevision.objects.none() + self.fields["current_revision"].widget = forms.HiddenInput() class ArticleAdmin(admin.ModelAdmin): inlines = [ArticleRevisionInline] form = ArticleForm - search_fields = ('current_revision__title', 'current_revision__content') + search_fields = ("current_revision__title", "current_revision__content") class URLPathAdmin(MPTTModelAdmin): inlines = [ArticleObjectAdmin] - list_filter = ('site', 'articles__article__current_revision__deleted', - 'articles__article__created', - 'articles__article__modified') - list_display = ('__str__', 'article', 'get_created') - raw_id_fields = ('article',) + list_filter = ( + "site", + "articles__article__current_revision__deleted", + "articles__article__created", + "articles__article__modified", + ) + list_display = ("__str__", "article", "get_created") + raw_id_fields = ("article",) def get_created(self, instance): return instance.article.created - get_created.short_description = _('created') + + get_created.short_description = _("created") def save_model(self, request, obj, form, change): """ diff --git a/src/wiki/apps.py b/src/wiki/apps.py index cd5ec15a..c1e7866f 100644 --- a/src/wiki/apps.py +++ b/src/wiki/apps.py @@ -7,13 +7,22 @@ from . import checks class WikiConfig(AppConfig): - default_site = 'wiki.sites.WikiSite' + default_site = "wiki.sites.WikiSite" name = "wiki" verbose_name = _("Wiki") def ready(self): - register(checks.check_for_required_installed_apps, checks.Tags.required_installed_apps) - register(checks.check_for_obsolete_installed_apps, checks.Tags.obsolete_installed_apps) + register( + checks.check_for_required_installed_apps, + checks.Tags.required_installed_apps, + ) + register( + checks.check_for_obsolete_installed_apps, + checks.Tags.obsolete_installed_apps, + ) register(checks.check_for_context_processors, checks.Tags.context_processors) - register(checks.check_for_fields_in_custom_user_model, checks.Tags.fields_in_custom_user_model) + register( + checks.check_for_fields_in_custom_user_model, + checks.Tags.fields_in_custom_user_model, + ) load_wiki_plugins() diff --git a/src/wiki/checks.py b/src/wiki/checks.py index 68e840fe..36c35918 100644 --- a/src/wiki/checks.py +++ b/src/wiki/checks.py @@ -12,29 +12,29 @@ class Tags: REQUIRED_INSTALLED_APPS = ( # module name, package name, error code - ('mptt', 'django-mptt', 'E001'), - ('sekizai', 'django-sekizai', 'E002'), - ('django.contrib.humanize', 'django.contrib.humanize', 'E003'), - ('django.contrib.contenttypes', 'django.contrib.contenttypes', 'E004'), - ('django.contrib.sites', 'django.contrib.sites', 'E005'), + ("mptt", "django-mptt", "E001"), + ("sekizai", "django-sekizai", "E002"), + ("django.contrib.humanize", "django.contrib.humanize", "E003"), + ("django.contrib.contenttypes", "django.contrib.contenttypes", "E004"), + ("django.contrib.sites", "django.contrib.sites", "E005"), ) OBSOLETE_INSTALLED_APPS = ( # obsolete module name, new module name, error code - ('django_notify', 'django_nyt', 'E006'), + ("django_notify", "django_nyt", "E006"), ) REQUIRED_CONTEXT_PROCESSORS = ( # context processor name, error code - ('django.contrib.auth.context_processors.auth', 'E007'), - ('django.template.context_processors.request', 'E008'), - ('sekizai.context_processors.sekizai', 'E009'), + ("django.contrib.auth.context_processors.auth", "E007"), + ("django.template.context_processors.request", "E008"), + ("sekizai.context_processors.sekizai", "E009"), ) FIELDS_IN_CUSTOM_USER_MODEL = ( # check function, field fetcher, required field type, error code - ('check_user_field', 'USERNAME_FIELD', 'CharField', 'E010'), - ('check_email_field', 'get_email_field_name()', 'EmailField', 'E011'), + ("check_user_field", "USERNAME_FIELD", "CharField", "E010"), + ("check_email_field", "get_email_field_name()", "EmailField", "E011"), ) @@ -43,10 +43,7 @@ def check_for_required_installed_apps(app_configs, **kwargs): for app in REQUIRED_INSTALLED_APPS: if not apps.is_installed(app[0]): errors.append( - Error( - 'needs %s in INSTALLED_APPS' % app[1], - id='wiki.%s' % app[2], - ) + Error("needs %s in INSTALLED_APPS" % app[1], id="wiki.%s" % app[2],) ) return errors @@ -57,8 +54,9 @@ def check_for_obsolete_installed_apps(app_configs, **kwargs): if apps.is_installed(app[0]): errors.append( Error( - 'You need to change from %s to %s in INSTALLED_APPS and your urlconfig.' % (app[0], app[1]), - id='wiki.%s' % app[2], + "You need to change from %s to %s in INSTALLED_APPS and your urlconfig." + % (app[0], app[1]), + id="wiki.%s" % app[2], ) ) return errors @@ -71,8 +69,9 @@ def check_for_context_processors(app_configs, **kwargs): if context_processor[0] not in context_processors: errors.append( Error( - "needs %s in TEMPLATE['OPTIONS']['context_processors']" % context_processor[0], - id='wiki.%s' % context_processor[1], + "needs %s in TEMPLATE['OPTIONS']['context_processors']" + % context_processor[0], + id="wiki.%s" % context_processor[1], ) ) return errors @@ -81,20 +80,33 @@ def check_for_context_processors(app_configs, **kwargs): def check_for_fields_in_custom_user_model(app_configs, **kwargs): errors = [] from wiki.conf import settings + if not settings.ACCOUNT_HANDLING: return errors import wiki.forms_account_handling from django.contrib.auth import get_user_model + User = get_user_model() - for check_function_name, field_fetcher, required_field_type, error_code in FIELDS_IN_CUSTOM_USER_MODEL: + for ( + check_function_name, + field_fetcher, + required_field_type, + error_code, + ) in FIELDS_IN_CUSTOM_USER_MODEL: function = getattr(wiki.forms_account_handling, check_function_name) if not function(User): errors.append( Error( - '%s.%s.%s refers to a field that is not of type %s' % (User.__module__, User.__name__, field_fetcher, required_field_type), - hint='If you have your own login/logout views, turn off settings.WIKI_ACCOUNT_HANDLING', + "%s.%s.%s refers to a field that is not of type %s" + % ( + User.__module__, + User.__name__, + field_fetcher, + required_field_type, + ), + hint="If you have your own login/logout views, turn off settings.WIKI_ACCOUNT_HANDLING", obj=User, - id='wiki.%s' % error_code, + id="wiki.%s" % error_code, ) ) return errors diff --git a/src/wiki/conf/settings.py b/src/wiki/conf/settings.py index 87f4c857..4b8e6273 100644 --- a/src/wiki/conf/settings.py +++ b/src/wiki/conf/settings.py @@ -6,24 +6,18 @@ from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ #: Should urls be case sensitive? -URL_CASE_SENSITIVE = getattr(django_settings, 'WIKI_URL_CASE_SENSITIVE', False) +URL_CASE_SENSITIVE = getattr(django_settings, "WIKI_URL_CASE_SENSITIVE", False) # Non-configurable (at the moment) -WIKI_LANGUAGE = 'markdown' +WIKI_LANGUAGE = "markdown" #: The editor class to use -- maybe a 3rd party or your own...? You can always #: extend the built-in editor and customize it! -EDITOR = getattr( - django_settings, - 'WIKI_EDITOR', - 'wiki.editors.markitup.MarkItUp') +EDITOR = getattr(django_settings, "WIKI_EDITOR", "wiki.editors.markitup.MarkItUp") #: Whether to use Bleach or not. It's not recommended to turn this off unless #: you know what you're doing and you don't want to use the other options. -MARKDOWN_SANITIZE_HTML = getattr( - django_settings, - 'WIKI_MARKDOWN_SANITIZE_HTML', - True) +MARKDOWN_SANITIZE_HTML = getattr(django_settings, "WIKI_MARKDOWN_SANITIZE_HTML", True) #: Arguments for the Markdown instance, as a dictionary. The "extensions" key #: should be a list of extra extensions to use besides the built-in django-wiki @@ -48,130 +42,113 @@ MARKDOWN_SANITIZE_HTML = getattr( #: "wiki.core.markdown.mdx.responsivetable", "wiki.plugins.macros.mdx.macro", #: "wiki.plugins.macros.mdx.toc", "wiki.plugins.macros.mdx.wikilinks". MARKDOWN_KWARGS = { - 'extensions': [ - 'markdown.extensions.footnotes', - 'markdown.extensions.attr_list', - 'markdown.extensions.footnotes', - 'markdown.extensions.attr_list', - 'markdown.extensions.def_list', - 'markdown.extensions.tables', - 'markdown.extensions.abbr', - 'markdown.extensions.sane_lists', + "extensions": [ + "markdown.extensions.footnotes", + "markdown.extensions.attr_list", + "markdown.extensions.footnotes", + "markdown.extensions.attr_list", + "markdown.extensions.def_list", + "markdown.extensions.tables", + "markdown.extensions.abbr", + "markdown.extensions.sane_lists", ], - 'extension_configs': { - 'wiki.plugins.macros.mdx.toc': {'title': _('Contents')}, - }, + "extension_configs": {"wiki.plugins.macros.mdx.toc": {"title": _("Contents")},}, } -MARKDOWN_KWARGS.update(getattr(django_settings, 'WIKI_MARKDOWN_KWARGS', {})) - -_default_tag_whitelists = bleach.ALLOWED_TAGS + [ - 'figure', - 'figcaption', - 'br', - 'hr', - 'p', - 'div', - 'img', - 'pre', - 'span', - 'sup', - 'table', - 'thead', - 'tbody', - 'th', - 'tr', - 'td', - 'dl', - 'dt', - 'dd', -] + ['h{}'.format(n) for n in range(1, 7)] +MARKDOWN_KWARGS.update(getattr(django_settings, "WIKI_MARKDOWN_KWARGS", {})) + +_default_tag_whitelists = ( + bleach.ALLOWED_TAGS + + [ + "figure", + "figcaption", + "br", + "hr", + "p", + "div", + "img", + "pre", + "span", + "sup", + "table", + "thead", + "tbody", + "th", + "tr", + "td", + "dl", + "dt", + "dd", + ] + + ["h{}".format(n) for n in range(1, 7)] +) #: List of allowed tags in Markdown article contents. MARKDOWN_HTML_WHITELIST = _default_tag_whitelists -MARKDOWN_HTML_WHITELIST += ( - getattr( - django_settings, - 'WIKI_MARKDOWN_HTML_WHITELIST', - [] - ) -) +MARKDOWN_HTML_WHITELIST += getattr(django_settings, "WIKI_MARKDOWN_HTML_WHITELIST", []) _default_attribute_whitelist = bleach.ALLOWED_ATTRIBUTES for tag in MARKDOWN_HTML_WHITELIST: if tag not in _default_attribute_whitelist: _default_attribute_whitelist[tag] = [] - _default_attribute_whitelist[tag].append('class') - _default_attribute_whitelist[tag].append('id') - _default_attribute_whitelist[tag].append('target') - _default_attribute_whitelist[tag].append('rel') + _default_attribute_whitelist[tag].append("class") + _default_attribute_whitelist[tag].append("id") + _default_attribute_whitelist[tag].append("target") + _default_attribute_whitelist[tag].append("rel") -_default_attribute_whitelist['img'].append('src') -_default_attribute_whitelist['img'].append('alt') +_default_attribute_whitelist["img"].append("src") +_default_attribute_whitelist["img"].append("alt") #: Dictionary of allowed attributes in Markdown article contents. MARKDOWN_HTML_ATTRIBUTES = _default_attribute_whitelist MARKDOWN_HTML_ATTRIBUTES.update( - getattr( - django_settings, - 'WIKI_MARKDOWN_HTML_ATTRIBUTES', - {} - ) + getattr(django_settings, "WIKI_MARKDOWN_HTML_ATTRIBUTES", {}) ) #: Allowed inline styles in Markdown article contents, default is no styles #: (empty list). -MARKDOWN_HTML_STYLES = ( - getattr( - django_settings, - 'WIKI_MARKDOWN_HTML_STYLES', - [] - ) -) +MARKDOWN_HTML_STYLES = getattr(django_settings, "WIKI_MARKDOWN_HTML_STYLES", []) _project_defined_attrs = getattr( - django_settings, - 'WIKI_MARKDOWN_HTML_ATTRIBUTE_WHITELIST', - False) + django_settings, "WIKI_MARKDOWN_HTML_ATTRIBUTE_WHITELIST", False +) # If styles are allowed but no custom attributes are defined, we allow styles # for all kinds of tags. if MARKDOWN_HTML_STYLES and not _project_defined_attrs: - MARKDOWN_HTML_ATTRIBUTES['*'] = 'style' + MARKDOWN_HTML_ATTRIBUTES["*"] = "style" #: This slug is used in URLPath if an article has been deleted. The children of the #: URLPath of that article are moved to lost and found. They keep their permissions #: and all their content. LOST_AND_FOUND_SLUG = getattr( - django_settings, - 'WIKI_LOST_AND_FOUND_SLUG', - 'lost-and-found') + django_settings, "WIKI_LOST_AND_FOUND_SLUG", "lost-and-found" +) #: When True, this blocks new slugs that resolve to non-wiki views, stopping #: users creating articles that conflict with overlapping URLs from other apps. CHECK_SLUG_URL_AVAILABLE = getattr( - django_settings, - 'WIKI_CHECK_SLUG_URL_AVAILABLE', - True) + django_settings, "WIKI_CHECK_SLUG_URL_AVAILABLE", True +) #: Do we want to log IPs of anonymous users? -LOG_IPS_ANONYMOUS = getattr(django_settings, 'WIKI_LOG_IPS_ANONYMOUS', True) +LOG_IPS_ANONYMOUS = getattr(django_settings, "WIKI_LOG_IPS_ANONYMOUS", True) #: Do we want to log IPs of logged in users? -LOG_IPS_USERS = getattr(django_settings, 'WIKI_LOG_IPS_USERS', False) +LOG_IPS_USERS = getattr(django_settings, "WIKI_LOG_IPS_USERS", False) #: Mapping from message.level to bootstrap class names. MESSAGE_TAG_CSS_CLASS = getattr( django_settings, - 'WIKI_MESSAGE_TAG_CSS_CLASS', + "WIKI_MESSAGE_TAG_CSS_CLASS", { messages.DEBUG: "alert alert-info", messages.ERROR: "alert alert-danger", messages.INFO: "alert alert-info", messages.SUCCESS: "alert alert-success", messages.WARNING: "alert alert-warning", - } + }, ) #################################### @@ -184,68 +161,62 @@ MESSAGE_TAG_CSS_CLASS = getattr( #: A function returning True/False if a user has permission to #: read contents of an article and plugins. #: Relevance: Viewing articles and plugins. -CAN_READ = getattr(django_settings, 'WIKI_CAN_READ', None) +CAN_READ = getattr(django_settings, "WIKI_CAN_READ", None) #: A function returning True/False if a user has permission to #: change contents, i.e. add new revisions to an article. #: Often, plugins also use this. #: Relevance: Editing articles, changing revisions, editing plugins. -CAN_WRITE = getattr(django_settings, 'WIKI_CAN_WRITE', None) +CAN_WRITE = getattr(django_settings, "WIKI_CAN_WRITE", None) #: A function returning True/False if a user has permission to assign #: permissions on an article. #: Relevance: Changing owner and group membership. -CAN_ASSIGN = getattr(django_settings, 'WIKI_CAN_ASSIGN', None) +CAN_ASSIGN = getattr(django_settings, "WIKI_CAN_ASSIGN", None) #: A function returning True/False if the owner of an article has permission #: to change the group to a user's own groups. #: Relevance: Changing group membership. -CAN_ASSIGN_OWNER = getattr(django_settings, 'WIKI_ASSIGN_OWNER', None) +CAN_ASSIGN_OWNER = getattr(django_settings, "WIKI_ASSIGN_OWNER", None) #: A function returning True/False if a user has permission to change #: read/write access for groups and others. -CAN_CHANGE_PERMISSIONS = getattr( - django_settings, - 'WIKI_CAN_CHANGE_PERMISSIONS', - None) +CAN_CHANGE_PERMISSIONS = getattr(django_settings, "WIKI_CAN_CHANGE_PERMISSIONS", None) #: Specifies if a user has access to soft deletion of articles. -CAN_DELETE = getattr(django_settings, 'WIKI_CAN_DELETE', None) +CAN_DELETE = getattr(django_settings, "WIKI_CAN_DELETE", None) #: A function returning True/False if a user has permission to change #: moderate, ie. lock articles and permanently delete content. -CAN_MODERATE = getattr(django_settings, 'WIKI_CAN_MODERATE', None) +CAN_MODERATE = getattr(django_settings, "WIKI_CAN_MODERATE", None) #: A function returning True/False if a user has permission to create #: new groups and users for the wiki. -CAN_ADMIN = getattr(django_settings, 'WIKI_CAN_ADMIN', None) +CAN_ADMIN = getattr(django_settings, "WIKI_CAN_ADMIN", None) #: Treat anonymous (i.e. non logged in) users as the "other" user group. -ANONYMOUS = getattr(django_settings, 'WIKI_ANONYMOUS', True) +ANONYMOUS = getattr(django_settings, "WIKI_ANONYMOUS", True) #: Globally enable write access for anonymous users, if true anonymous users #: will be treated as the others_write boolean field on models.Article. -ANONYMOUS_WRITE = getattr(django_settings, 'WIKI_ANONYMOUS_WRITE', False) +ANONYMOUS_WRITE = getattr(django_settings, "WIKI_ANONYMOUS_WRITE", False) #: Globally enable create access for anonymous users. #: Defaults to ``ANONYMOUS_WRITE``. -ANONYMOUS_CREATE = getattr( - django_settings, - 'WIKI_ANONYMOUS_CREATE', - ANONYMOUS_WRITE) +ANONYMOUS_CREATE = getattr(django_settings, "WIKI_ANONYMOUS_CREATE", ANONYMOUS_WRITE) #: Default setting to allow anonymous users upload access. Used in #: plugins.attachments and plugins.images, and can be overwritten in #: these plugins. -ANONYMOUS_UPLOAD = getattr(django_settings, 'WIKI_ANONYMOUS_UPLOAD', False) +ANONYMOUS_UPLOAD = getattr(django_settings, "WIKI_ANONYMOUS_UPLOAD", False) #: Sign up, login and logout views should be accessible. -ACCOUNT_HANDLING = getattr(django_settings, 'WIKI_ACCOUNT_HANDLING', True) +ACCOUNT_HANDLING = getattr(django_settings, "WIKI_ACCOUNT_HANDLING", True) #: Signup allowed? If it's not allowed, logged in superusers can still access #: the signup page to create new users. ACCOUNT_SIGNUP_ALLOWED = ACCOUNT_HANDLING and getattr( - django_settings, 'WIKI_ACCOUNT_SIGNUP_ALLOWED', True + django_settings, "WIKI_ACCOUNT_SIGNUP_ALLOWED", True ) if ACCOUNT_HANDLING: @@ -264,72 +235,59 @@ else: #: Maximum amount of children to display in a menu before showing "+more". #: NEVER set this to 0 as it will wrongly inform the user that there are no #: children and for instance that an article can be safely deleted. -SHOW_MAX_CHILDREN = getattr(django_settings, 'WIKI_SHOW_MAX_CHILDREN', 20) +SHOW_MAX_CHILDREN = getattr(django_settings, "WIKI_SHOW_MAX_CHILDREN", 20) #: User Bootstrap's select widget. Switch off if you're not using Bootstrap! USE_BOOTSTRAP_SELECT_WIDGET = getattr( - django_settings, - 'WIKI_USE_BOOTSTRAP_SELECT_WIDGET', - True) + django_settings, "WIKI_USE_BOOTSTRAP_SELECT_WIDGET", True +) #: Dotted name of the class used to construct urlpatterns for the wiki. #: Default is wiki.urls.WikiURLPatterns. To customize urls or view handlers, #: you can derive from this. -URL_CONFIG_CLASS = getattr( - django_settings, - 'WIKI_URL_CONFIG_CLASS', - None) +URL_CONFIG_CLASS = getattr(django_settings, "WIKI_URL_CONFIG_CLASS", None) #: Seconds of timeout before renewing the article cache. Articles are automatically #: renewed whenever an edit occurs but article content may be generated from #: other objects that are changed. -CACHE_TIMEOUT = getattr(django_settings, 'WIKI_CACHE_TIMEOUT', 600) +CACHE_TIMEOUT = getattr(django_settings, "WIKI_CACHE_TIMEOUT", 600) #: Choose the Group model to use for permission handling. Defaults to django's auth.Group. -GROUP_MODEL = getattr(django_settings, 'WIKI_GROUP_MODEL', 'auth.Group') +GROUP_MODEL = getattr(django_settings, "WIKI_GROUP_MODEL", "auth.Group") ################### # SPAM PROTECTION # ################### #: Maximum allowed revisions per hour for any given user or IP. -REVISIONS_PER_HOUR = getattr(django_settings, 'WIKI_REVISIONS_PER_HOUR', 60) +REVISIONS_PER_HOUR = getattr(django_settings, "WIKI_REVISIONS_PER_HOUR", 60) #: Maximum allowed revisions per minute for any given user or IP. -REVISIONS_PER_MINUTES = getattr( - django_settings, - 'WIKI_REVISIONS_PER_MINUTES', - 5) +REVISIONS_PER_MINUTES = getattr(django_settings, "WIKI_REVISIONS_PER_MINUTES", 5) #: Maximum allowed revisions per hour for any anonymous user and any IP. REVISIONS_PER_HOUR_ANONYMOUS = getattr( - django_settings, - 'WIKI_REVISIONS_PER_HOUR_ANONYMOUS', - 10) + django_settings, "WIKI_REVISIONS_PER_HOUR_ANONYMOUS", 10 +) #: Maximum allowed revisions per minute for any anonymous user and any IP. REVISIONS_PER_MINUTES_ANONYMOUS = getattr( - django_settings, - 'WIKI_REVISIONS_PER_MINUTES_ANONYMOUS', - 2) + django_settings, "WIKI_REVISIONS_PER_MINUTES_ANONYMOUS", 2 +) #: Number of minutes to look back for looking up ``REVISIONS_PER_MINUTES`` #: and ``REVISIONS_PER_MINUTES_ANONYMOUS``. REVISIONS_MINUTES_LOOKBACK = getattr( - django_settings, - 'WIKI_REVISIONS_MINUTES_LOOKBACK', - 2) + django_settings, "WIKI_REVISIONS_MINUTES_LOOKBACK", 2 +) ########### # STORAGE # ########### #: Default Django storage backend to use for images, attachments etc. -STORAGE_BACKEND = getattr( - django_settings, - 'WIKI_STORAGE_BACKEND', - default_storage) +STORAGE_BACKEND = getattr(django_settings, "WIKI_STORAGE_BACKEND", default_storage) #: Use django-sendfile for sending out files? Otherwise the whole file is #: first read into memory and than send with a mime type based on the file. -USE_SENDFILE = getattr(django_settings, 'WIKI_ATTACHMENTS_USE_SENDFILE', False) +USE_SENDFILE = getattr(django_settings, "WIKI_ATTACHMENTS_USE_SENDFILE", False) diff --git a/src/wiki/core/exceptions.py b/src/wiki/core/exceptions.py index b60e3640..3ca008dd 100644 --- a/src/wiki/core/exceptions.py +++ b/src/wiki/core/exceptions.py @@ -1,10 +1,10 @@ - # If no root URL is found, we raise this... class NoRootURL(Exception): pass + # If there is more than one... diff --git a/src/wiki/core/http.py b/src/wiki/core/http.py index 589478f1..0fbe6fad 100644 --- a/src/wiki/core/http.py +++ b/src/wiki/core/http.py @@ -11,6 +11,7 @@ from wiki.conf import settings def django_sendfile_response(request, filepath): from sendfile import sendfile + return sendfile(request, filepath) @@ -23,18 +24,18 @@ def send_file(request, filepath, last_modified=None, filename=None): else: mimetype, encoding = mimetypes.guess_type(fullpath) - mimetype = mimetype or 'application/octet-stream' + mimetype = mimetype or "application/octet-stream" if settings.USE_SENDFILE: response = django_sendfile_response(request, filepath) else: - response = HttpResponse(open(fullpath, 'rb').read(), content_type=mimetype) + response = HttpResponse(open(fullpath, "rb").read(), content_type=mimetype) if not last_modified: response["Last-Modified"] = http_date(statobj.st_mtime) else: if isinstance(last_modified, datetime): - last_modified = float(dateformat.format(last_modified, 'U')) + last_modified = float(dateformat.format(last_modified, "U")) response["Last-Modified"] = http_date(epoch_seconds=last_modified) response["Content-Length"] = statobj.st_size @@ -44,9 +45,11 @@ def send_file(request, filepath, last_modified=None, filename=None): if filename: filename_escaped = filepath_to_uri(filename) - if 'pdf' in mimetype.lower(): + if "pdf" in mimetype.lower(): response["Content-Disposition"] = "inline; filename=%s" % filename_escaped else: - response["Content-Disposition"] = "attachment; filename=%s" % filename_escaped + response["Content-Disposition"] = ( + "attachment; filename=%s" % filename_escaped + ) return response diff --git a/src/wiki/core/markdown/__init__.py b/src/wiki/core/markdown/__init__.py index a6201382..f282c37d 100644 --- a/src/wiki/core/markdown/__init__.py +++ b/src/wiki/core/markdown/__init__.py @@ -5,10 +5,9 @@ from wiki.core.plugins import registry as plugin_registry class ArticleMarkdown(markdown.Markdown): - def __init__(self, article, preview=False, user=None, *args, **kwargs): kwargs.update(settings.MARKDOWN_KWARGS) - kwargs['extensions'] = self.get_markdown_extensions() + kwargs["extensions"] = self.get_markdown_extensions() super().__init__(*args, **kwargs) self.article = article self.preview = preview @@ -17,13 +16,13 @@ class ArticleMarkdown(markdown.Markdown): def core_extensions(self): """List of core extensions found in the mdx folder""" return [ - 'wiki.core.markdown.mdx.codehilite', - 'wiki.core.markdown.mdx.previewlinks', - 'wiki.core.markdown.mdx.responsivetable', + "wiki.core.markdown.mdx.codehilite", + "wiki.core.markdown.mdx.previewlinks", + "wiki.core.markdown.mdx.responsivetable", ] def get_markdown_extensions(self): - extensions = list(settings.MARKDOWN_KWARGS.get('extensions', [])) + extensions = list(settings.MARKDOWN_KWARGS.get("extensions", [])) extensions += self.core_extensions() extensions += plugin_registry.get_markdown_extensions() return extensions @@ -31,17 +30,16 @@ class ArticleMarkdown(markdown.Markdown): def convert(self, text, *args, **kwargs): html = super().convert(text, *args, **kwargs) if settings.MARKDOWN_SANITIZE_HTML: - tags = settings.MARKDOWN_HTML_WHITELIST + plugin_registry.get_html_whitelist() + tags = ( + settings.MARKDOWN_HTML_WHITELIST + plugin_registry.get_html_whitelist() + ) attrs = dict() attrs.update(settings.MARKDOWN_HTML_ATTRIBUTES) attrs.update(plugin_registry.get_html_attributes().items()) html = bleach.clean( - html, - tags=tags, - attributes=attrs, - styles=settings.MARKDOWN_HTML_STYLES, + html, tags=tags, attributes=attrs, styles=settings.MARKDOWN_HTML_STYLES, ) return html diff --git a/src/wiki/core/markdown/mdx/codehilite.py b/src/wiki/core/markdown/mdx/codehilite.py index cd62325d..b5e8fb05 100644 --- a/src/wiki/core/markdown/mdx/codehilite.py +++ b/src/wiki/core/markdown/mdx/codehilite.py @@ -11,13 +11,13 @@ logger = logging.getLogger(__name__) def highlight(code, config, tab_length, lang=None): code = CodeHilite( code, - linenums=config['linenums'], - guess_lang=config['guess_lang'], - css_class=config['css_class'], - style=config['pygments_style'], - noclasses=config['noclasses'], + linenums=config["linenums"], + guess_lang=config["guess_lang"], + css_class=config["css_class"], + style=config["pygments_style"], + noclasses=config["noclasses"], tab_length=tab_length, - use_pygments=config['use_pygments'], + use_pygments=config["use_pygments"], lang=lang, ) html = code.hilite() @@ -31,15 +31,19 @@ class WikiFencedBlockPreprocessor(Preprocessor): directly and without configuration options invoke the vanilla CodeHilite extension. """ - FENCED_BLOCK_RE = re.compile(r''' + + FENCED_BLOCK_RE = re.compile( + r""" (?P^(?:~{3,}|`{3,}))[ ]* # Opening ``` or ~~~ (\{?\.?(?P[a-zA-Z0-9_+-]*))?[ ]* # Optional {, and lang # Optional highlight lines, single- or double-quote-delimited (hl_lines=(?P"|')(?P.*?)(?P=quot))?[ ]* }?[ ]*\n # Optional closing } (?P.*?)(?<=\n) -(?P=fence)[ ]*$''', re.MULTILINE | re.DOTALL | re.VERBOSE) - CODE_WRAP = '
%s
' +(?P=fence)[ ]*$""", + re.MULTILINE | re.DOTALL | re.VERBOSE, + ) + CODE_WRAP = "
%s
" def __init__(self, md): super().__init__(md) @@ -54,14 +58,14 @@ class WikiFencedBlockPreprocessor(Preprocessor): while 1: m = self.FENCED_BLOCK_RE.search(text) if m: - lang = '' - if m.group('lang'): - lang = m.group('lang') - html = highlight(m.group('code'), self.config, self.markdown.tab_length, lang=lang) + lang = "" + if m.group("lang"): + lang = m.group("lang") + html = highlight( + m.group("code"), self.config, self.markdown.tab_length, lang=lang + ) placeholder = self.markdown.htmlStash.store(html) - text = '%s\n%s\n%s' % (text[:m.start()], - placeholder, - text[m.end():]) + text = "%s\n%s\n%s" % (text[: m.start()], placeholder, text[m.end() :]) else: break return text.split("\n") @@ -72,16 +76,16 @@ class HiliteTreeprocessor(Treeprocessor): def run(self, root): """ Find code blocks and store in htmlStash. """ - blocks = root.iter('pre') + blocks = root.iter("pre") for block in blocks: - if len(block) == 1 and block[0].tag == 'code': + if len(block) == 1 and block[0].tag == "code": html = highlight(block[0].text, self.config, self.markdown.tab_length) placeholder = self.markdown.htmlStash.store(html) # Clear codeblock in etree instance block.clear() # Change to p element which will later # be removed when inserting raw html - block.tag = 'p' + block.tag = "p" block.text = placeholder @@ -112,9 +116,7 @@ class WikiCodeHiliteExtension(CodeHiliteExtension): del md.preprocessors["fenced_code_block"] hiliter = WikiFencedBlockPreprocessor(md) hiliter.config = self.getConfigs() - md.preprocessors.add('fenced_code_block', - hiliter, - ">normalize_whitespace") + md.preprocessors.add("fenced_code_block", hiliter, ">normalize_whitespace") md.registerExtension(self) diff --git a/src/wiki/core/markdown/mdx/previewlinks.py b/src/wiki/core/markdown/mdx/previewlinks.py index f1ec92d2..dea3db40 100644 --- a/src/wiki/core/markdown/mdx/previewlinks.py +++ b/src/wiki/core/markdown/mdx/previewlinks.py @@ -7,17 +7,16 @@ class PreviewLinksExtension(markdown.Extension): """Markdown Extension that sets all anchor targets to _blank when in preview mode""" def extendMarkdown(self, md): - md.treeprocessors.add('previewlinks', PreviewLinksTree(md), "_end") + md.treeprocessors.add("previewlinks", PreviewLinksTree(md), "_end") class PreviewLinksTree(Treeprocessor): - def run(self, root): if self.md.preview: - for a in root.findall('.//a'): + for a in root.findall(".//a"): # Do not set target for links like href='#markdown' - if not a.get('href').startswith('#'): - a.set('target', '_blank') + if not a.get("href").startswith("#"): + a.set("target", "_blank") return root diff --git a/src/wiki/core/markdown/mdx/responsivetable.py b/src/wiki/core/markdown/mdx/responsivetable.py index 8ba493ae..86a224f1 100644 --- a/src/wiki/core/markdown/mdx/responsivetable.py +++ b/src/wiki/core/markdown/mdx/responsivetable.py @@ -7,12 +7,12 @@ class ResponsiveTableExtension(markdown.Extension): """Wraps all tables with Bootstrap's table-responsive class""" def extendMarkdown(self, md): - md.treeprocessors.add('responsivetable', ResponsiveTableTree(md), "_end") + md.treeprocessors.add("responsivetable", ResponsiveTableTree(md), "_end") class ResponsiveTableTree(Treeprocessor): def run(self, root): - for table_wrapper in list(root.getiterator('table')): + for table_wrapper in list(root.getiterator("table")): table_new = self.create_table_element() self.convert_to_wrapper(table_wrapper) self.move_children(table_wrapper, table_new) @@ -21,9 +21,9 @@ class ResponsiveTableTree(Treeprocessor): def create_table_element(self): """Create table element with text and tail""" - element = etree.Element('table') - element.text = '\n' - element.tail = '\n' + element = etree.Element("table") + element.text = "\n" + element.tail = "\n" return element def move_children(self, element1, element2): @@ -35,8 +35,8 @@ class ResponsiveTableTree(Treeprocessor): element1.remove(child) def convert_to_wrapper(self, element): - element.tag = 'div' - element.set('class', 'table-responsive') + element.tag = "div" + element.set("class", "table-responsive") def makeExtension(*args, **kwargs): diff --git a/src/wiki/core/paginator.py b/src/wiki/core/paginator.py index 63aa97e6..93bb1bb5 100644 --- a/src/wiki/core/paginator.py +++ b/src/wiki/core/paginator.py @@ -2,12 +2,11 @@ from django.core.paginator import Paginator class WikiPaginator(Paginator): - def __init__(self, *args, **kwargs): """ :param side_pages: How many pages should be shown before and after the current page """ - self.side_pages = kwargs.pop('side_pages', 4) + self.side_pages = kwargs.pop("side_pages", 4) super().__init__(*args, **kwargs) def page(self, number): @@ -18,7 +17,9 @@ class WikiPaginator(Paginator): @property def page_range(self): left = max(self.last_accessed_page_number - self.side_pages, 2) - right = min(self.last_accessed_page_number + self.side_pages + 1, self.num_pages) + right = min( + self.last_accessed_page_number + self.side_pages + 1, self.num_pages + ) pages = [] if self.num_pages > 0: pages = [1] diff --git a/src/wiki/core/permissions.py b/src/wiki/core/permissions.py index 140bfde7..1a47e849 100644 --- a/src/wiki/core/permissions.py +++ b/src/wiki/core/permissions.py @@ -17,7 +17,9 @@ def can_read(article, user): return settings.CAN_READ(article, user) else: # Deny reading access to deleted articles if user has no delete access - article_is_deleted = article.current_revision and article.current_revision.deleted + article_is_deleted = ( + article.current_revision and article.current_revision.deleted + ) if article_is_deleted and not article.can_delete(user): return False @@ -31,8 +33,7 @@ def can_read(article, user): if user == article.owner: return True if article.group_read: - if article.group and user.groups.filter( - id=article.group.id).exists(): + if article.group and user.groups.filter(id=article.group.id).exists(): return True if article.can_moderate(user): return True @@ -52,8 +53,7 @@ def can_write(article, user): if user == article.owner: return True if article.group_write: - if article.group and user and user.groups.filter( - id=article.group.id).exists(): + if article.group and user and user.groups.filter(id=article.group.id).exists(): return True if article.can_moderate(user): return True @@ -63,7 +63,7 @@ def can_write(article, user): def can_assign(article, user): if callable(settings.CAN_ASSIGN): return settings.CAN_ASSIGN(article, user) - return not user.is_anonymous and user.has_perm('wiki.assign') + return not user.is_anonymous and user.has_perm("wiki.assign") def can_assign_owner(article, user): @@ -75,11 +75,8 @@ def can_assign_owner(article, user): def can_change_permissions(article, user): if callable(settings.CAN_CHANGE_PERMISSIONS): return settings.CAN_CHANGE_PERMISSIONS(article, user) - return ( - not user.is_anonymous and ( - article.owner == user or - user.has_perm('wiki.assign') - ) + return not user.is_anonymous and ( + article.owner == user or user.has_perm("wiki.assign") ) @@ -92,10 +89,10 @@ def can_delete(article, user): def can_moderate(article, user): if callable(settings.CAN_MODERATE): return settings.CAN_MODERATE(article, user) - return not user.is_anonymous and user.has_perm('wiki.moderate') + return not user.is_anonymous and user.has_perm("wiki.moderate") def can_admin(article, user): if callable(settings.CAN_ADMIN): return settings.CAN_ADMIN(article, user) - return not user.is_anonymous and user.has_perm('wiki.admin') + return not user.is_anonymous and user.has_perm("wiki.admin") diff --git a/src/wiki/core/plugins/base.py b/src/wiki/core/plugins/base.py index b07ea3bf..5e5668ab 100644 --- a/src/wiki/core/plugins/base.py +++ b/src/wiki/core/plugins/base.py @@ -16,6 +16,7 @@ plugin's models. class BasePlugin: """Plugins should inherit from this""" + # Must fill in! slug = None @@ -23,10 +24,10 @@ class BasePlugin: settings_form = None # A form class to add to the settings tab urlpatterns = { # General urlpatterns that will reside in /wiki/plugins/plugin-slug/... - 'root': [], + "root": [], # urlpatterns that receive article_id or urlpath, i.e. # /wiki/ArticleName/plugin/plugin-slug/... - 'article': [], + "article": [], } article_tab = None # (_('Attachments'), "fa fa-file") article_view = None # A view for article_id/plugin/slug/ @@ -50,17 +51,17 @@ class BasePlugin: class PluginSidebarFormMixin(forms.ModelForm): - unsaved_article_title = forms.CharField(widget=forms.HiddenInput(), - required=True) - unsaved_article_content = forms.CharField(widget=forms.HiddenInput(), - required=False) + unsaved_article_title = forms.CharField(widget=forms.HiddenInput(), required=True) + unsaved_article_content = forms.CharField( + widget=forms.HiddenInput(), required=False + ) def get_usermessage(self): pass class PluginSettingsFormMixin: - settings_form_headline = _('Settings for plugin') + settings_form_headline = _("Settings for plugin") settings_order = 1 settings_write_access = False diff --git a/src/wiki/core/plugins/loader.py b/src/wiki/core/plugins/loader.py index b716ef1e..cc7769c8 100644 --- a/src/wiki/core/plugins/loader.py +++ b/src/wiki/core/plugins/loader.py @@ -2,4 +2,4 @@ from django.utils.module_loading import autodiscover_modules def load_wiki_plugins(): - autodiscover_modules('wiki_plugin') + autodiscover_modules("wiki_plugin") diff --git a/src/wiki/core/plugins/registry.py b/src/wiki/core/plugins/registry.py index 75dac1fd..b07b01e0 100644 --- a/src/wiki/core/plugins/registry.py +++ b/src/wiki/core/plugins/registry.py @@ -19,7 +19,7 @@ def register(PluginClass): plugin = PluginClass() _cache[PluginClass] = plugin - settings_form = getattr(PluginClass, 'settings_form', None) + settings_form = getattr(PluginClass, "settings_form", None) if settings_form: if isinstance(settings_form, str): klassname = settings_form.split(".")[-1] @@ -28,29 +28,17 @@ def register(PluginClass): settings_form = getattr(form_module, klassname) _settings_forms.append(settings_form) - if getattr(PluginClass, 'article_tab', None): + if getattr(PluginClass, "article_tab", None): _article_tabs.append(plugin) - if getattr(PluginClass, 'sidebar', None): + if getattr(PluginClass, "sidebar", None): _sidebar.append(plugin) - _markdown_extensions.extend( - getattr( - PluginClass, - 'markdown_extensions', - [])) - - _html_whitelist.extend( - getattr( - PluginClass, - 'html_whitelist', - [])) - - _html_attributes.update( - getattr( - PluginClass, - 'html_attributes', - dict())) + _markdown_extensions.extend(getattr(PluginClass, "markdown_extensions", [])) + + _html_whitelist.extend(getattr(PluginClass, "html_whitelist", [])) + + _html_attributes.update(getattr(PluginClass, "html_attributes", dict())) def get_plugins(): diff --git a/src/wiki/core/utils.py b/src/wiki/core/utils.py index 957701e6..e7e6b626 100644 --- a/src/wiki/core/utils.py +++ b/src/wiki/core/utils.py @@ -7,5 +7,5 @@ def object_to_json_response(obj, status=200): version of that object """ return JsonResponse( - data=obj, status=status, safe=False, json_dumps_params={'ensure_ascii': False}, + data=obj, status=status, safe=False, json_dumps_params={"ensure_ascii": False}, ) diff --git a/src/wiki/core/version.py b/src/wiki/core/version.py index f6a0d50b..df3daccd 100644 --- a/src/wiki/core/version.py +++ b/src/wiki/core/version.py @@ -46,14 +46,14 @@ def get_version(version=None): major = get_major_version(version) - sub = '' - if version[3] == 'alpha' and version[4] == 0: + sub = "" + if version[3] == "alpha" and version[4] == 0: git_changeset = get_git_changeset() if git_changeset: - sub = '.dev%s' % git_changeset + sub = ".dev%s" % git_changeset - elif version[3] != 'final': - mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'} + elif version[3] != "final": + mapping = {"alpha": "a", "beta": "b", "rc": "c"} sub = mapping[version[3]] + str(version[4]) return str(major + sub) @@ -63,7 +63,7 @@ def get_major_version(version=None): "Returns major version from VERSION." version = get_complete_version(version) parts = 2 if version[2] == 0 else 3 - major = '.'.join(str(x) for x in version[:parts]) + major = ".".join(str(x) for x in version[:parts]) return major @@ -75,17 +75,17 @@ def get_complete_version(version=None): from wiki import VERSION as version else: assert len(version) == 5 - assert version[3] in ('alpha', 'beta', 'rc', 'final') + assert version[3] in ("alpha", "beta", "rc", "final") return version def get_docs_version(version=None): version = get_complete_version(version) - if version[3] != 'final': - return 'dev' + if version[3] != "final": + return "dev" else: - return '%d.%d' % version[:2] + return "%d.%d" % version[:2] def get_git_changeset(): @@ -97,13 +97,16 @@ def get_git_changeset(): """ repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) git_log = subprocess.Popen( - 'git log --pretty=format:%ct --quiet -1 HEAD', - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - shell=True, cwd=repo_dir, universal_newlines=True + "git log --pretty=format:%ct --quiet -1 HEAD", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + cwd=repo_dir, + universal_newlines=True, ) timestamp = git_log.communicate()[0] try: timestamp = datetime.datetime.utcfromtimestamp(int(timestamp)) except ValueError: return None - return timestamp.strftime('%Y%m%d%H%M%S') + return timestamp.strftime("%Y%m%d%H%M%S") diff --git a/src/wiki/decorators.py b/src/wiki/decorators.py index e5433e5e..935c0e9b 100644 --- a/src/wiki/decorators.py +++ b/src/wiki/decorators.py @@ -1,6 +1,10 @@ from functools import wraps -from django.http import HttpResponseForbidden, HttpResponseNotFound, HttpResponseRedirect +from django.http import ( + HttpResponseForbidden, + HttpResponseNotFound, + HttpResponseRedirect, +) from django.shortcuts import get_object_or_404, redirect from django.template.loader import render_to_string from django.urls import reverse @@ -11,31 +15,37 @@ from wiki.core.exceptions import NoRootURL def response_forbidden(request, article, urlpath, read_denied=False): if request.user.is_anonymous: - qs = request.META.get('QUERY_STRING', '') + qs = request.META.get("QUERY_STRING", "") if qs: - qs = urlquote('?' + qs) + qs = urlquote("?" + qs) else: - qs = '' + qs = "" return redirect(settings.LOGIN_URL + "?next=" + request.path + qs) else: return HttpResponseForbidden( render_to_string( "wiki/permission_denied.html", context={ - 'article': article, - 'urlpath': urlpath, - 'read_denied': read_denied + "article": article, + "urlpath": urlpath, + "read_denied": read_denied, }, - request=request + request=request, ) ) # TODO: This decorator is too complex (C901) -def get_article(func=None, can_read=True, can_write=False, # noqa: max-complexity=13 - deleted_contents=False, not_locked=False, - can_delete=False, can_moderate=False, - can_create=False): +def get_article( + func=None, + can_read=True, + can_write=False, # noqa: max-complexity=13 + deleted_contents=False, + not_locked=False, + can_delete=False, + can_moderate=False, + can_create=False, +): """View decorator for processing standard url keyword args: Intercepts the keyword args path or article_id and looks up an article, calling the decorated func with this ID. @@ -64,35 +74,32 @@ def get_article(func=None, can_read=True, can_write=False, # noqa: max-complexi def wrapper(request, *args, **kwargs): from . import models - path = kwargs.pop('path', None) - article_id = kwargs.pop('article_id', None) + path = kwargs.pop("path", None) + article_id = kwargs.pop("article_id", None) # fetch by urlpath.path if path is not None: try: urlpath = models.URLPath.get_by_path(path, select_related=True) except NoRootURL: - return redirect('wiki:root_create') + return redirect("wiki:root_create") except models.URLPath.DoesNotExist: try: - pathlist = list( - filter( - lambda x: x != "", - path.split("/"), - )) + pathlist = list(filter(lambda x: x != "", path.split("/"),)) path = "/".join(pathlist[:-1]) parent = models.URLPath.get_by_path(path) return HttpResponseRedirect( - reverse( - "wiki:create", kwargs={'path': parent.path, }) + "?slug=%s" % pathlist[-1].lower()) + reverse("wiki:create", kwargs={"path": parent.path,}) + + "?slug=%s" % pathlist[-1].lower() + ) except models.URLPath.DoesNotExist: return HttpResponseNotFound( render_to_string( "wiki/error.html", - context={ - 'error_type': 'ancestors_missing' - }, - request=request)) + context={"error_type": "ancestors_missing"}, + request=request, + ) + ) if urlpath.article: # urlpath is already smart about prefetching items on article # (like current_revision), so we don't have to @@ -100,7 +107,7 @@ def get_article(func=None, can_read=True, can_write=False, # noqa: max-complexi else: # Be robust: Somehow article is gone but urlpath exists... # clean up - return_url = reverse('wiki:get', kwargs={'path': urlpath.parent.path}) + return_url = reverse("wiki:get", kwargs={"path": urlpath.parent.path}) urlpath.delete() return HttpResponseRedirect(return_url) @@ -114,20 +121,23 @@ def get_article(func=None, can_read=True, can_write=False, # noqa: max-complexi article = get_object_or_404(articles, id=article_id) try: urlpath = models.URLPath.objects.get(articles__article=article) - except (models.URLPath.DoesNotExist, models.URLPath.MultipleObjectsReturned): + except ( + models.URLPath.DoesNotExist, + models.URLPath.MultipleObjectsReturned, + ): urlpath = None else: - raise TypeError('You should specify either article_id or path') + raise TypeError("You should specify either article_id or path") if not deleted_contents: # If the article has been deleted, show a special page. if urlpath: if urlpath.is_deleted(): # This also checks all ancestors - return redirect('wiki:deleted', path=urlpath.path) + return redirect("wiki:deleted", path=urlpath.path) else: if article.current_revision and article.current_revision.deleted: - return redirect('wiki:deleted', article_id=article.id) + return redirect("wiki:deleted", article_id=article.id) if article.current_revision.locked and not_locked: return response_forbidden(request, article, urlpath) @@ -139,7 +149,8 @@ def get_article(func=None, can_read=True, can_write=False, # noqa: max-complexi return response_forbidden(request, article, urlpath) if can_create and not ( - request.user.is_authenticated or settings.ANONYMOUS_CREATE): + request.user.is_authenticated or settings.ANONYMOUS_CREATE + ): return response_forbidden(request, article, urlpath) if can_delete and not article.can_delete(request.user): @@ -148,7 +159,7 @@ def get_article(func=None, can_read=True, can_write=False, # noqa: max-complexi if can_moderate and not article.can_moderate(request.user): return response_forbidden(request, article, urlpath) - kwargs['urlpath'] = urlpath + kwargs["urlpath"] = urlpath return func(request, article, *args, **kwargs) @@ -163,7 +174,8 @@ def get_article(func=None, can_read=True, can_write=False, # noqa: max-complexi not_locked=not_locked, can_delete=can_delete, can_moderate=can_moderate, - can_create=can_create) + can_create=can_create, + ) def disable_signal_for_loaddata(signal_handler): @@ -173,7 +185,8 @@ def disable_signal_for_loaddata(signal_handler): @wraps(signal_handler) def wrapper(*args, **kwargs): - if kwargs.get('raw', False): + if kwargs.get("raw", False): return return signal_handler(*args, **kwargs) + return wrapper diff --git a/src/wiki/editors/base.py b/src/wiki/editors/base.py index 67164e03..3896eedd 100644 --- a/src/wiki/editors/base.py +++ b/src/wiki/editors/base.py @@ -7,7 +7,7 @@ class BaseEditor: # The editor id can be used for conditional testing. If you write your # own editor class, you can use the same editor_id as some editor - editor_id = 'plaintext' + editor_id = "plaintext" media_admin = () media_frontend = () diff --git a/src/wiki/editors/markitup.py b/src/wiki/editors/markitup.py index d29e55d2..ea23330d 100644 --- a/src/wiki/editors/markitup.py +++ b/src/wiki/editors/markitup.py @@ -8,9 +8,9 @@ class MarkItUpWidget(forms.Widget): def __init__(self, attrs=None): # The 'rows' and 'cols' attributes are required for HTML correctness. default_attrs = { - 'class': 'markItUp', - 'rows': '10', - 'cols': '40', + "class": "markItUp", + "rows": "10", + "cols": "40", } if attrs: default_attrs.update(attrs) @@ -24,7 +24,7 @@ class MarkItUpAdminWidget(MarkItUpWidget): class MarkItUp(BaseEditor): - editor_id = 'markitup' + editor_id = "markitup" def get_admin_widget(self, instance=None): return MarkItUpAdminWidget() @@ -34,20 +34,26 @@ class MarkItUp(BaseEditor): class AdminMedia: css = { - 'all': ("wiki/markitup/skins/simple/style.css", - "wiki/markitup/sets/admin/style.css",) + "all": ( + "wiki/markitup/skins/simple/style.css", + "wiki/markitup/sets/admin/style.css", + ) } - js = ("wiki/markitup/admin.init.js", - "wiki/markitup/jquery.markitup.js", - "wiki/markitup/sets/admin/set.js", - ) + js = ( + "wiki/markitup/admin.init.js", + "wiki/markitup/jquery.markitup.js", + "wiki/markitup/sets/admin/set.js", + ) class Media: css = { - 'all': ("wiki/markitup/skins/simple/style.css", - "wiki/markitup/sets/frontend/style.css",) + "all": ( + "wiki/markitup/skins/simple/style.css", + "wiki/markitup/sets/frontend/style.css", + ) } - js = ("wiki/markitup/frontend.init.js", - "wiki/markitup/jquery.markitup.js", - "wiki/markitup/sets/frontend/set.js", - ) + js = ( + "wiki/markitup/frontend.init.js", + "wiki/markitup/jquery.markitup.js", + "wiki/markitup/sets/frontend/set.js", + ) diff --git a/src/wiki/forms.py b/src/wiki/forms.py index c83555d7..738362d8 100644 --- a/src/wiki/forms.py +++ b/src/wiki/forms.py @@ -1,19 +1,18 @@ - __all__ = [ - 'UserCreationForm', - 'UserUpdateForm', - 'WikiSlugField', - 'SpamProtectionMixin', - 'CreateRootForm', - 'MoveForm', - 'EditForm', - 'SelectWidgetBootstrap', - 'TextInputPrepend', - 'CreateForm', - 'DeleteForm', - 'PermissionsForm', - 'DirFilterForm', - 'SearchForm', + "UserCreationForm", + "UserUpdateForm", + "WikiSlugField", + "SpamProtectionMixin", + "CreateRootForm", + "MoveForm", + "EditForm", + "SelectWidgetBootstrap", + "TextInputPrepend", + "CreateForm", + "DeleteForm", + "PermissionsForm", + "DirFilterForm", + "SearchForm", ] from datetime import timedelta @@ -39,10 +38,10 @@ from wiki.editors import getEditor from .forms_account_handling import UserCreationForm, UserUpdateForm validate_slug_numbers = RegexValidator( - r'^[0-9]+$', + r"^[0-9]+$", _("A 'slug' cannot consist solely of numbers."), - 'invalid', - inverse_match=True + "invalid", + inverse_match=True, ) @@ -55,50 +54,48 @@ class WikiSlugField(forms.CharField): default_validators = [validators.validate_slug, validate_slug_numbers] def __init__(self, *args, **kwargs): - self.allow_unicode = kwargs.pop('allow_unicode', False) + self.allow_unicode = kwargs.pop("allow_unicode", False) if self.allow_unicode: self.default_validators = [ validators.validate_unicode_slug, - validate_slug_numbers + validate_slug_numbers, ] super().__init__(*args, **kwargs) def _clean_slug(slug, urlpath): if slug.startswith("_"): - raise forms.ValidationError( - gettext('A slug may not begin with an underscore.')) - if slug == 'admin': - raise forms.ValidationError( - gettext("'admin' is not a permitted slug name.")) + raise forms.ValidationError(gettext("A slug may not begin with an underscore.")) + if slug == "admin": + raise forms.ValidationError(gettext("'admin' is not a permitted slug name.")) if settings.URL_CASE_SENSITIVE: - already_existing_slug = models.URLPath.objects.filter( - slug=slug, - parent=urlpath) + already_existing_slug = models.URLPath.objects.filter(slug=slug, parent=urlpath) else: slug = slug.lower() already_existing_slug = models.URLPath.objects.filter( - slug__iexact=slug, - parent=urlpath) + slug__iexact=slug, parent=urlpath + ) if already_existing_slug: already_urlpath = already_existing_slug[0] if already_urlpath.article and already_urlpath.article.current_revision.deleted: raise forms.ValidationError( - gettext('A deleted article with slug "%s" already exists.') % - already_urlpath.slug) + gettext('A deleted article with slug "%s" already exists.') + % already_urlpath.slug + ) else: raise forms.ValidationError( - gettext('A slug named "%s" already exists.') % - already_urlpath.slug) + gettext('A slug named "%s" already exists.') % already_urlpath.slug + ) if settings.CHECK_SLUG_URL_AVAILABLE: try: # Fail validation if URL resolves to non-wiki app - match = resolve(urlpath.path + '/' + slug + '/') - if match.app_name != 'wiki': + match = resolve(urlpath.path + "/" + slug + "/") + if match.app_name != "wiki": raise forms.ValidationError( - gettext('This slug conflicts with an existing URL.')) + gettext("This slug conflicts with an existing URL.") + ) except Resolver404: pass @@ -128,19 +125,20 @@ class SpamProtectionMixin: if request.user.is_authenticated: user = request.user else: - ip_address = request.META.get('REMOTE_ADDR', None) + ip_address = request.META.get("REMOTE_ADDR", None) if not (user or ip_address): raise forms.ValidationError( gettext( - 'Spam protection failed to find both a logged in user and an IP address.')) + "Spam protection failed to find both a logged in user and an IP address." + ) + ) def check_interval(from_time, max_count, interval_name): - from_time = timezone.now( - ) - timedelta(minutes=settings.REVISIONS_MINUTES_LOOKBACK) - revisions = self.revision_model.objects.filter( - created__gte=from_time, + from_time = timezone.now() - timedelta( + minutes=settings.REVISIONS_MINUTES_LOOKBACK ) + revisions = self.revision_model.objects.filter(created__gte=from_time,) if user: revisions = revisions.filter(user=user) if ip_address: @@ -148,17 +146,20 @@ class SpamProtectionMixin: revisions = revisions.count() if revisions >= max_count: raise forms.ValidationError( - gettext('Spam protection: You are only allowed to create or edit %(revisions)d article(s) per %(interval_name)s.') % { - 'revisions': max_count, - 'interval_name': interval_name, - }) + gettext( + "Spam protection: You are only allowed to create or edit %(revisions)d article(s) per %(interval_name)s." + ) + % {"revisions": max_count, "interval_name": interval_name,} + ) if not settings.LOG_IPS_ANONYMOUS: return - if request.user.has_perm('wiki.moderator'): + if request.user.has_perm("wiki.moderator"): return - from_time = timezone.now() - timedelta(minutes=settings.REVISIONS_MINUTES_LOOKBACK) + from_time = timezone.now() - timedelta( + minutes=settings.REVISIONS_MINUTES_LOOKBACK + ) if request.user.is_authenticated: per_minute = settings.REVISIONS_PER_MINUTES else: @@ -166,9 +167,9 @@ class SpamProtectionMixin: check_interval( from_time, per_minute, - _('minute') if settings.REVISIONS_MINUTES_LOOKBACK == 1 else ( - _('%d minutes') % - settings.REVISIONS_MINUTES_LOOKBACK), + _("minute") + if settings.REVISIONS_MINUTES_LOOKBACK == 1 + else (_("%d minutes") % settings.REVISIONS_MINUTES_LOOKBACK), ) from_time = timezone.now() - timedelta(minutes=60) @@ -176,75 +177,85 @@ class SpamProtectionMixin: per_hour = settings.REVISIONS_PER_MINUTES else: per_hour = settings.REVISIONS_PER_MINUTES_ANONYMOUS - check_interval(from_time, per_hour, _('hour')) + check_interval(from_time, per_hour, _("hour")) class CreateRootForm(forms.Form): title = forms.CharField( - label=_('Title'), + label=_("Title"), help_text=_( - 'Initial title of the article. May be overridden with revision titles.')) + "Initial title of the article. May be overridden with revision titles." + ), + ) content = forms.CharField( - label=_('Type in some contents'), + label=_("Type in some contents"), help_text=_( - 'This is just the initial contents of your article. After creating it, you can use more complex features like adding plugins, meta data, related articles etc...'), - required=False, widget=getEditor().get_widget()) # @UndefinedVariable + "This is just the initial contents of your article. After creating it, you can use more complex features like adding plugins, meta data, related articles etc..." + ), + required=False, + widget=getEditor().get_widget(), + ) # @UndefinedVariable class MoveForm(forms.Form): - destination = forms.CharField(label=_('Destination')) + destination = forms.CharField(label=_("Destination")) slug = WikiSlugField(max_length=models.URLPath.SLUG_MAX_LENGTH) - redirect = forms.BooleanField(label=_('Redirect pages'), - help_text=_('Create a redirect page for every moved article?'), - required=False) + redirect = forms.BooleanField( + label=_("Redirect pages"), + help_text=_("Create a redirect page for every moved article?"), + required=False, + ) def clean(self): cd = super().clean() - if cd.get('slug'): - dest_path = get_object_or_404(models.URLPath, pk=self.cleaned_data['destination']) - cd['slug'] = _clean_slug(cd['slug'], dest_path) + if cd.get("slug"): + dest_path = get_object_or_404( + models.URLPath, pk=self.cleaned_data["destination"] + ) + cd["slug"] = _clean_slug(cd["slug"], dest_path) return cd class EditForm(forms.Form, SpamProtectionMixin): - title = forms.CharField(label=_('Title'),) + title = forms.CharField(label=_("Title"),) content = forms.CharField( - label=_('Contents'), - required=False, - widget=getEditor().get_widget()) # @UndefinedVariable + label=_("Contents"), required=False, widget=getEditor().get_widget() + ) # @UndefinedVariable summary = forms.CharField( - label=pgettext_lazy('Revision comment', 'Summary'), + label=pgettext_lazy("Revision comment", "Summary"), help_text=_( - 'Give a short reason for your edit, which will be stated in the revision log.'), - required=False) - - current_revision = forms.IntegerField( + "Give a short reason for your edit, which will be stated in the revision log." + ), required=False, - widget=forms.HiddenInput()) + ) + + current_revision = forms.IntegerField(required=False, widget=forms.HiddenInput()) def __init__(self, request, current_revision, *args, **kwargs): self.request = request - self.no_clean = kwargs.pop('no_clean', False) - self.preview = kwargs.pop('preview', False) + self.no_clean = kwargs.pop("no_clean", False) + self.preview = kwargs.pop("preview", False) self.initial_revision = current_revision self.presumed_revision = None if current_revision: # For e.g. editing a section of the text: The content provided by the caller is used. # Otherwise use the content of the revision. provided_content = True - content = kwargs.pop('content', None) + content = kwargs.pop("content", None) if content is None: provided_content = False content = current_revision.content - initial = {'content': content, - 'title': current_revision.title, - 'current_revision': current_revision.id} - initial.update(kwargs.get('initial', {})) + initial = { + "content": content, + "title": current_revision.title, + "current_revision": current_revision.id, + } + initial.update(kwargs.get("initial", {})) # Manipulate any data put in args[0] such that the current_revision # is reset to match the actual current revision. @@ -253,34 +264,38 @@ class EditForm(forms.Form, SpamProtectionMixin): data = args[0] args = args[1:] if data is None: - data = kwargs.get('data', None) + data = kwargs.get("data", None) if data: - self.presumed_revision = data.get('current_revision', None) + self.presumed_revision = data.get("current_revision", None) if not str(self.presumed_revision) == str(self.initial_revision.id): newdata = {} for k, v in data.items(): newdata[k] = v - newdata['current_revision'] = self.initial_revision.id + newdata["current_revision"] = self.initial_revision.id # Don't merge if content comes from the caller if provided_content: self.presumed_revision = self.initial_revision.id else: - newdata['content'] = simple_merge(content, data.get('content', "")) - newdata['title'] = current_revision.title - kwargs['data'] = newdata + newdata["content"] = simple_merge( + content, data.get("content", "") + ) + newdata["title"] = current_revision.title + kwargs["data"] = newdata else: # Always pass as kwarg - kwargs['data'] = data + kwargs["data"] = data - kwargs['initial'] = initial + kwargs["initial"] = initial super().__init__(*args, **kwargs) def clean_title(self): - title = self.cleaned_data.get('title', None) + title = self.cleaned_data.get("title", None) title = (title or "").strip() if not title: - raise forms.ValidationError(gettext('Article is missing title or has an invalid title')) + raise forms.ValidationError( + gettext("Article is missing title or has an invalid title") + ) return title def clean(self): @@ -293,11 +308,15 @@ class EditForm(forms.Form, SpamProtectionMixin): if not str(self.initial_revision.id) == str(self.presumed_revision): raise forms.ValidationError( gettext( - 'While you were editing, someone else changed the revision. Your contents have been automatically merged with the new contents. Please review the text below.')) - if ('title' in self.cleaned_data and - self.cleaned_data['title'] == self.initial_revision.title and - self.cleaned_data['content'] == self.initial_revision.content): - raise forms.ValidationError(gettext('No changes made. Nothing to save.')) + "While you were editing, someone else changed the revision. Your contents have been automatically merged with the new contents. Please review the text below." + ) + ) + if ( + "title" in self.cleaned_data + and self.cleaned_data["title"] == self.initial_revision.title + and self.cleaned_data["content"] == self.initial_revision.content + ): + raise forms.ValidationError(gettext("No changes made. Nothing to save.")) self.check_spam() return self.cleaned_data @@ -312,21 +331,21 @@ class SelectWidgetBootstrap(forms.Select): option_template_name = "wiki/forms/select_option.html" def __init__(self, attrs={}, choices=(), disabled=False): - attrs['class'] = 'btn-group pull-left btn-group-form' + attrs["class"] = "btn-group pull-left btn-group-form" self.disabled = disabled self.noscript_widget = forms.Select(attrs={}, choices=choices) super().__init__(attrs, choices) def __setattr__(self, k, value): super().__setattr__(k, value) - if k not in ('attrs', 'disabled'): + if k not in ("attrs", "disabled"): self.noscript_widget.__setattr__(k, value) def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) - context['label'] = _('Select an option') - context['noscript'] = self.noscript_widget.render(name, value, {}) - context['disabled'] = ' disabled' if self.disabled else '' + context["label"] = _("Select an option") + context["noscript"] = self.noscript_widget.render(name, value, {}) + context["disabled"] = " disabled" if self.disabled else "" return context class Media(forms.Media): @@ -338,40 +357,41 @@ class TextInputPrepend(forms.TextInput): template_name = "wiki/forms/text.html" def __init__(self, *args, **kwargs): - self.prepend = kwargs.pop('prepend', "") + self.prepend = kwargs.pop("prepend", "") super().__init__(*args, **kwargs) def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) - context['prepend'] = mark_safe(self.prepend) + context["prepend"] = mark_safe(self.prepend) return context class CreateForm(forms.Form, SpamProtectionMixin): - def __init__(self, request, urlpath_parent, *args, **kwargs): super().__init__(*args, **kwargs) self.request = request self.urlpath_parent = urlpath_parent - title = forms.CharField(label=_('Title'),) + title = forms.CharField(label=_("Title"),) slug = WikiSlugField( - label=_('Slug'), + label=_("Slug"), help_text=_( - "This will be the address where your article can be found. Use only alphanumeric characters and - or _.
Note: If you change the slug later on, links pointing to this article are not updated."), - max_length=models.URLPath.SLUG_MAX_LENGTH) + "This will be the address where your article can be found. Use only alphanumeric characters and - or _.
Note: If you change the slug later on, links pointing to this article are not updated." + ), + max_length=models.URLPath.SLUG_MAX_LENGTH, + ) content = forms.CharField( - label=_('Contents'), - required=False, - widget=getEditor().get_widget()) # @UndefinedVariable + label=_("Contents"), required=False, widget=getEditor().get_widget() + ) # @UndefinedVariable summary = forms.CharField( - label=pgettext_lazy('Revision comment', 'Summary'), + label=pgettext_lazy("Revision comment", "Summary"), help_text=_("Write a brief message for the article's history log."), - required=False) + required=False, + ) def clean_slug(self): - return _clean_slug(self.cleaned_data['slug'], self.urlpath_parent) + return _clean_slug(self.cleaned_data["slug"], self.urlpath_parent) def clean(self): self.check_spam() @@ -379,80 +399,95 @@ class CreateForm(forms.Form, SpamProtectionMixin): class DeleteForm(forms.Form): - def __init__(self, *args, **kwargs): - self.article = kwargs.pop('article') - self.has_children = kwargs.pop('has_children') + self.article = kwargs.pop("article") + self.has_children = kwargs.pop("has_children") super().__init__(*args, **kwargs) - confirm = forms.BooleanField(required=False, label=_('Yes, I am sure')) + confirm = forms.BooleanField(required=False, label=_("Yes, I am sure")) purge = forms.BooleanField( widget=HiddenInput(), - required=False, label=_('Purge'), + required=False, + label=_("Purge"), help_text=_( - 'Purge the article: Completely remove it (and all its contents) with no undo. Purging is a good idea if you want to free the slug such that users can create new articles in its place.')) - revision = forms.ModelChoiceField(models.ArticleRevision.objects.all(), - widget=HiddenInput(), required=False) + "Purge the article: Completely remove it (and all its contents) with no undo. Purging is a good idea if you want to free the slug such that users can create new articles in its place." + ), + ) + revision = forms.ModelChoiceField( + models.ArticleRevision.objects.all(), widget=HiddenInput(), required=False + ) def clean(self): - if not self.cleaned_data['confirm']: - raise forms.ValidationError(gettext('You are not sure enough!')) - if self.cleaned_data['revision'] != self.article.current_revision: + if not self.cleaned_data["confirm"]: + raise forms.ValidationError(gettext("You are not sure enough!")) + if self.cleaned_data["revision"] != self.article.current_revision: raise forms.ValidationError( - gettext('While you tried to delete this article, it was modified. TAKE CARE!')) + gettext( + "While you tried to delete this article, it was modified. TAKE CARE!" + ) + ) return self.cleaned_data class PermissionsForm(PluginSettingsFormMixin, forms.ModelForm): locked = forms.BooleanField( - label=_('Lock article'), - help_text=_('Deny all users access to edit this article.'), - required=False) + label=_("Lock article"), + help_text=_("Deny all users access to edit this article."), + required=False, + ) - settings_form_headline = _('Permissions') + settings_form_headline = _("Permissions") settings_order = 5 settings_write_access = False owner_username = forms.CharField( required=False, - label=_('Owner'), - help_text=_('Enter the username of the owner.')) + label=_("Owner"), + help_text=_("Enter the username of the owner."), + ) group = forms.ModelChoiceField( - Group.objects.all(), - empty_label=_('(none)'), - label=_('Group'), - required=False) + Group.objects.all(), empty_label=_("(none)"), label=_("Group"), required=False + ) if settings.USE_BOOTSTRAP_SELECT_WIDGET: group.widget = SelectWidgetBootstrap() recursive = forms.BooleanField( - label=_('Inherit permissions'), - help_text=_('Check here to apply the above permissions (excluding group and owner of the article) recursively to articles below this one.'), - required=False) + label=_("Inherit permissions"), + help_text=_( + "Check here to apply the above permissions (excluding group and owner of the article) recursively to articles below this one." + ), + required=False, + ) recursive_owner = forms.BooleanField( - label=_('Inherit owner'), - help_text=_('Check here to apply the ownership setting recursively to articles below this one.'), - required=False) + label=_("Inherit owner"), + help_text=_( + "Check here to apply the ownership setting recursively to articles below this one." + ), + required=False, + ) recursive_group = forms.BooleanField( - label=_('Inherit group'), - help_text=_('Check here to apply the group setting recursively to articles below this one.'), - required=False) + label=_("Inherit group"), + help_text=_( + "Check here to apply the group setting recursively to articles below this one." + ), + required=False, + ) def get_usermessage(self): if self.changed_data: - return _('Permission settings for the article were updated.') + return _("Permission settings for the article were updated.") else: - return _('Your permission settings were unchanged, so nothing saved.') + return _("Your permission settings were unchanged, so nothing saved.") def __init__(self, article, request, *args, **kwargs): self.article = article self.user = request.user self.request = request - kwargs['instance'] = article - kwargs['initial'] = {'locked': article.current_revision.locked} + kwargs["instance"] = article + kwargs["initial"] = {"locked": article.current_revision.locked} super().__init__(*args, **kwargs) @@ -462,48 +497,49 @@ class PermissionsForm(PluginSettingsFormMixin, forms.ModelForm): if permissions.can_assign(article, request.user): self.can_assign = True self.can_change_groups = True - self.fields['group'].queryset = Group.objects.all() + self.fields["group"].queryset = Group.objects.all() elif permissions.can_assign_owner(article, request.user): - self.fields['group'].queryset = Group.objects.filter( - user=request.user) + self.fields["group"].queryset = Group.objects.filter(user=request.user) self.can_change_groups = True else: # Quick-fix... # Set the group dropdown to readonly and with the current # group as only selectable option - self.fields['group'] = forms.ModelChoiceField( - queryset=Group.objects.filter( - id=self.instance.group.id) - if self.instance.group else Group.objects.none(), - empty_label=_('(none)'), - required=False, widget=SelectWidgetBootstrap( - disabled=True) - if settings.USE_BOOTSTRAP_SELECT_WIDGET else forms.Select( - attrs={'disabled': True})) - self.fields['group_read'].widget = forms.HiddenInput() - self.fields['group_write'].widget = forms.HiddenInput() + self.fields["group"] = forms.ModelChoiceField( + queryset=Group.objects.filter(id=self.instance.group.id) + if self.instance.group + else Group.objects.none(), + empty_label=_("(none)"), + required=False, + widget=SelectWidgetBootstrap(disabled=True) + if settings.USE_BOOTSTRAP_SELECT_WIDGET + else forms.Select(attrs={"disabled": True}), + ) + self.fields["group_read"].widget = forms.HiddenInput() + self.fields["group_write"].widget = forms.HiddenInput() if not self.can_assign: - self.fields['owner_username'].widget = forms.TextInput(attrs={'readonly': 'true'}) - self.fields['recursive'].widget = forms.HiddenInput() - self.fields['recursive_group'].widget = forms.HiddenInput() - self.fields['recursive_owner'].widget = forms.HiddenInput() - self.fields['locked'].widget = forms.HiddenInput() + self.fields["owner_username"].widget = forms.TextInput( + attrs={"readonly": "true"} + ) + self.fields["recursive"].widget = forms.HiddenInput() + self.fields["recursive_group"].widget = forms.HiddenInput() + self.fields["recursive_owner"].widget = forms.HiddenInput() + self.fields["locked"].widget = forms.HiddenInput() - self.fields['owner_username'].initial = getattr( - article.owner, - User.USERNAME_FIELD) if article.owner else "" + self.fields["owner_username"].initial = ( + getattr(article.owner, User.USERNAME_FIELD) if article.owner else "" + ) def clean_owner_username(self): if self.can_assign: - username = self.cleaned_data['owner_username'] + username = self.cleaned_data["owner_username"] if username: try: kwargs = {User.USERNAME_FIELD: username} user = User.objects.get(**kwargs) except User.DoesNotExist: - raise forms.ValidationError( - gettext('No user with that username')) + raise forms.ValidationError(gettext("No user with that username")) else: user = None else: @@ -516,7 +552,7 @@ class PermissionsForm(PluginSettingsFormMixin, forms.ModelForm): # Alter the owner according to the form field owner_username # TODO: Why not rename this field to 'owner' so this happens # automatically? - article.owner = self.cleaned_data['owner_username'] + article.owner = self.cleaned_data["owner_username"] # Revert any changes to group permissions if the # current user is not allowed (see __init__) @@ -527,25 +563,24 @@ class PermissionsForm(PluginSettingsFormMixin, forms.ModelForm): article.group_write = self.article.group_write if self.can_assign: - if self.cleaned_data['recursive']: + if self.cleaned_data["recursive"]: article.set_permissions_recursive() - if self.cleaned_data['recursive_owner']: + if self.cleaned_data["recursive_owner"]: article.set_owner_recursive() - if self.cleaned_data['recursive_group']: + if self.cleaned_data["recursive_group"]: article.set_group_recursive() - if self.cleaned_data[ - 'locked'] and not article.current_revision.locked: + if self.cleaned_data["locked"] and not article.current_revision.locked: revision = models.ArticleRevision() revision.inherit_predecessor(self.article) revision.set_from_request(self.request) - revision.automatic_log = _('Article locked for editing') + revision.automatic_log = _("Article locked for editing") revision.locked = True self.article.add_revision(revision) - elif not self.cleaned_data['locked'] and article.current_revision.locked: + elif not self.cleaned_data["locked"] and article.current_revision.locked: revision = models.ArticleRevision() revision.inherit_predecessor(self.article) revision.set_from_request(self.request) - revision.automatic_log = _('Article unlocked for editing') + revision.automatic_log = _("Article unlocked for editing") revision.locked = False self.article.add_revision(revision) @@ -554,16 +589,17 @@ class PermissionsForm(PluginSettingsFormMixin, forms.ModelForm): class Meta: model = models.Article fields = ( - 'locked', - 'owner_username', - 'recursive_owner', - 'group', - 'recursive_group', - 'group_read', - 'group_write', - 'other_read', - 'other_write', - 'recursive') + "locked", + "owner_username", + "recursive_owner", + "group", + "recursive_group", + "group_read", + "group_write", + "other_read", + "other_write", + "recursive", + ) widgets = {} @@ -571,17 +607,17 @@ class DirFilterForm(forms.Form): query = forms.CharField( widget=forms.TextInput( - attrs={ - 'placeholder': _('Filter...'), - 'class': 'search-query form-control'}), - required=False) + attrs={"placeholder": _("Filter..."), "class": "search-query form-control"} + ), + required=False, + ) class SearchForm(forms.Form): q = forms.CharField( widget=forms.TextInput( - attrs={ - 'placeholder': _('Search...'), - 'class': 'search-query'}), - required=False) + attrs={"placeholder": _("Search..."), "class": "search-query"} + ), + required=False, + ) diff --git a/src/wiki/forms_account_handling.py b/src/wiki/forms_account_handling.py index 4432d54f..a4be76ae 100644 --- a/src/wiki/forms_account_handling.py +++ b/src/wiki/forms_account_handling.py @@ -26,14 +26,20 @@ def check_user_field(user_model): def check_email_field(user_model): - return isinstance(_get_field(user_model, user_model.get_email_field_name()), EmailField) + return isinstance( + _get_field(user_model, user_model.get_email_field_name()), EmailField + ) # django parses the ModelForm (and Meta classes) on class creation, which fails with custom models without expected fields. # We need to check this here, because if this module can't load then system checks can't run. -CustomUser = User \ - if (settings.ACCOUNT_HANDLING and check_user_field(User) and check_email_field(User)) \ +CustomUser = ( + User + if ( + settings.ACCOUNT_HANDLING and check_user_field(User) and check_email_field(User) + ) else django.contrib.auth.models.User +) class UserCreationForm(UserCreationForm): @@ -44,16 +50,16 @@ class UserCreationForm(UserCreationForm): # Add honeypots self.honeypot_fieldnames = "address", "phone" - self.honeypot_class = ''.join( - random.choice(string.ascii_uppercase + string.digits) - for __ in range(10)) - self.honeypot_jsfunction = 'f' + ''.join( - random.choice(string.ascii_uppercase + string.digits) - for __ in range(10)) + self.honeypot_class = "".join( + random.choice(string.ascii_uppercase + string.digits) for __ in range(10) + ) + self.honeypot_jsfunction = "f" + "".join( + random.choice(string.ascii_uppercase + string.digits) for __ in range(10) + ) for fieldname in self.honeypot_fieldnames: self.fields[fieldname] = forms.CharField( - widget=forms.TextInput(attrs={'class': self.honeypot_class}), + widget=forms.TextInput(attrs={"class": self.honeypot_class}), required=False, ) @@ -61,7 +67,8 @@ class UserCreationForm(UserCreationForm): for fieldname in self.honeypot_fieldnames: if self.cleaned_data[fieldname]: raise forms.ValidationError( - "Thank you, non-human visitor. Please keep trying to fill in the form.") + "Thank you, non-human visitor. Please keep trying to fill in the form." + ) return self.cleaned_data class Meta: @@ -70,12 +77,16 @@ class UserCreationForm(UserCreationForm): class UserUpdateForm(forms.ModelForm): - password1 = forms.CharField(label="New password", widget=forms.PasswordInput(), required=False) - password2 = forms.CharField(label="Confirm password", widget=forms.PasswordInput(), required=False) + password1 = forms.CharField( + label="New password", widget=forms.PasswordInput(), required=False + ) + password2 = forms.CharField( + label="Confirm password", widget=forms.PasswordInput(), required=False + ) def clean(self): - password1 = self.cleaned_data.get('password1') - password2 = self.cleaned_data.get('password2') + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") if password1 and password1 != password2: raise forms.ValidationError(_("Passwords don't match")) diff --git a/src/wiki/managers.py b/src/wiki/managers.py index 7ea0fb65..b5dd2f32 100644 --- a/src/wiki/managers.py +++ b/src/wiki/managers.py @@ -5,33 +5,34 @@ from mptt.managers import TreeManager class ArticleQuerySet(QuerySet): - def can_read(self, user): """Filter objects so only the ones with a user's reading access are included""" - if user.has_perm('wiki.moderator'): + if user.has_perm("wiki.moderator"): return self if user.is_anonymous: q = self.filter(other_read=True) else: - q = self.filter(Q(other_read=True) | - Q(owner=user) | - (Q(group__user=user) & Q(group_read=True)) - ).annotate(Count('id')) + q = self.filter( + Q(other_read=True) + | Q(owner=user) + | (Q(group__user=user) & Q(group_read=True)) + ).annotate(Count("id")) return q def can_write(self, user): """Filter objects so only the ones with a user's writing access are included""" - if user.has_perm('wiki.moderator'): + if user.has_perm("wiki.moderator"): return self if user.is_anonymous: q = self.filter(other_write=True) else: - q = self.filter(Q(other_write=True) | - Q(owner=user) | - (Q(group__user=user) & Q(group_write=True)) - ) + q = self.filter( + Q(other_write=True) + | Q(owner=user) + | (Q(group__user=user) & Q(group_write=True)) + ) return q def active(self): @@ -39,7 +40,6 @@ class ArticleQuerySet(QuerySet): class ArticleEmptyQuerySet(EmptyQuerySet): - def can_read(self, user): return self @@ -51,35 +51,36 @@ class ArticleEmptyQuerySet(EmptyQuerySet): class ArticleFkQuerySetMixin: - def can_read(self, user): """Filter objects so only the ones with a user's reading access are included""" - if user.has_perm('wiki.moderate'): + if user.has_perm("wiki.moderate"): return self if user.is_anonymous: q = self.filter(article__other_read=True) else: # https://github.com/django-wiki/django-wiki/issues/67 q = self.filter( - Q(article__other_read=True) | Q(article__owner=user) | - (Q(article__group__user=user) & Q( - article__group_read=True))).annotate(Count('id')) + Q(article__other_read=True) + | Q(article__owner=user) + | (Q(article__group__user=user) & Q(article__group_read=True)) + ).annotate(Count("id")) return q def can_write(self, user): """Filter objects so only the ones with a user's writing access are included""" - if user.has_perm('wiki.moderate'): + if user.has_perm("wiki.moderate"): return self if user.is_anonymous: q = self.filter(article__other_write=True) else: # https://github.com/django-wiki/django-wiki/issues/67 q = self.filter( - Q(article__other_write=True) | Q(article__owner=user) | - (Q(article__group__user=user) & Q( - article__group_write=True))).annotate(Count('id')) + Q(article__other_write=True) + | Q(article__owner=user) + | (Q(article__group__user=user) & Q(article__group_write=True)) + ).annotate(Count("id")) return q def active(self): @@ -87,7 +88,6 @@ class ArticleFkQuerySetMixin: class ArticleFkEmptyQuerySetMixin: - def can_read(self, user): return self @@ -107,7 +107,6 @@ class ArticleFkEmptyQuerySet(ArticleFkEmptyQuerySetMixin, EmptyQuerySet): class ArticleManager(models.Manager): - def get_empty_query_set(self): return self.get_queryset().none() @@ -125,7 +124,6 @@ class ArticleManager(models.Manager): class ArticleFkManager(models.Manager): - def get_empty_query_set(self): return self.get_queryset().none() @@ -143,7 +141,6 @@ class ArticleFkManager(models.Manager): class URLPathEmptyQuerySet(EmptyQuerySet, ArticleFkEmptyQuerySetMixin): - def select_related_common(self): return self @@ -152,27 +149,25 @@ class URLPathEmptyQuerySet(EmptyQuerySet, ArticleFkEmptyQuerySetMixin): class URLPathQuerySet(QuerySet, ArticleFkQuerySetMixin): - def select_related_common(self): return self.select_related( - "parent", - "article__current_revision", - "article__owner") + "parent", "article__current_revision", "article__owner" + ) def default_order(self): """Returns elements by there article order""" - return self.order_by('article__current_revision__title') + return self.order_by("article__current_revision__title") class URLPathManager(TreeManager): - def get_empty_query_set(self): return self.get_queryset().none() def get_queryset(self): """Return a QuerySet with the same ordering as the TreeManager.""" return URLPathQuerySet(self.model, using=self._db).order_by( - self.tree_id_attr, self.left_attr) + self.tree_id_attr, self.left_attr + ) def select_related_common(self): return self.get_queryset().common_select_related() diff --git a/src/wiki/migrations/0001_initial.py b/src/wiki/migrations/0001_initial.py index 3b2f17d5..e3210e23 100644 --- a/src/wiki/migrations/0001_initial.py +++ b/src/wiki/migrations/0001_initial.py @@ -9,189 +9,436 @@ from wiki.conf.settings import GROUP_MODEL class Migration(migrations.Migration): dependencies = [ - ('sites', '0001_initial'), + ("sites", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contenttypes', '0001_initial'), - ('auth', '0001_initial'), + ("contenttypes", "0001_initial"), + ("auth", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Article', + name="Article", fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('created', models.DateTimeField(verbose_name='created', auto_now_add=True)), - ('modified', models.DateTimeField(verbose_name='modified', auto_now=True, help_text='Article properties last modified')), - ('group_read', models.BooleanField(default=True, verbose_name='group read access')), - ('group_write', models.BooleanField(default=True, verbose_name='group write access')), - ('other_read', models.BooleanField(default=True, verbose_name='others read access')), - ('other_write', models.BooleanField(default=True, verbose_name='others write access')), + ( + "id", + models.AutoField( + serialize=False, + primary_key=True, + auto_created=True, + verbose_name="ID", + ), + ), + ( + "created", + models.DateTimeField(verbose_name="created", auto_now_add=True), + ), + ( + "modified", + models.DateTimeField( + verbose_name="modified", + auto_now=True, + help_text="Article properties last modified", + ), + ), + ( + "group_read", + models.BooleanField(default=True, verbose_name="group read access"), + ), + ( + "group_write", + models.BooleanField( + default=True, verbose_name="group write access" + ), + ), + ( + "other_read", + models.BooleanField( + default=True, verbose_name="others read access" + ), + ), + ( + "other_write", + models.BooleanField( + default=True, verbose_name="others write access" + ), + ), ], options={ - 'permissions': (('moderate', 'Can edit all articles and lock/unlock/restore'), ('assign', 'Can change ownership of any article'), ('grant', 'Can assign permissions to other users')), + "permissions": ( + ("moderate", "Can edit all articles and lock/unlock/restore"), + ("assign", "Can change ownership of any article"), + ("grant", "Can assign permissions to other users"), + ), }, bases=(models.Model,), ), migrations.CreateModel( - name='ArticleForObject', + name="ArticleForObject", fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('object_id', models.PositiveIntegerField(verbose_name='object ID')), - ('is_mptt', models.BooleanField(default=False, editable=False)), - ('article', models.ForeignKey(to='wiki.Article', on_delete=models.CASCADE)), - ('content_type', models.ForeignKey(related_name='content_type_set_for_articleforobject', verbose_name='content type', to='contenttypes.ContentType', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + serialize=False, + primary_key=True, + auto_created=True, + verbose_name="ID", + ), + ), + ("object_id", models.PositiveIntegerField(verbose_name="object ID")), + ("is_mptt", models.BooleanField(default=False, editable=False)), + ( + "article", + models.ForeignKey(to="wiki.Article", on_delete=models.CASCADE), + ), + ( + "content_type", + models.ForeignKey( + related_name="content_type_set_for_articleforobject", + verbose_name="content type", + to="contenttypes.ContentType", + on_delete=models.CASCADE, + ), + ), ], options={ - 'verbose_name_plural': 'Articles for object', - 'verbose_name': 'Article for object', + "verbose_name_plural": "Articles for object", + "verbose_name": "Article for object", }, bases=(models.Model,), ), migrations.CreateModel( - name='ArticlePlugin', + name="ArticlePlugin", fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('deleted', models.BooleanField(default=False)), - ('created', models.DateTimeField(auto_now_add=True)), + ( + "id", + models.AutoField( + serialize=False, + primary_key=True, + auto_created=True, + verbose_name="ID", + ), + ), + ("deleted", models.BooleanField(default=False)), + ("created", models.DateTimeField(auto_now_add=True)), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.CreateModel( - name='ArticleRevision', + name="ArticleRevision", fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('revision_number', models.IntegerField(verbose_name='revision number', editable=False)), - ('user_message', models.TextField(blank=True)), - ('automatic_log', models.TextField(blank=True, editable=False)), - ('ip_address', IPAddressField(null=True, verbose_name='IP address', blank=True, editable=False)), - ('modified', models.DateTimeField(auto_now=True)), - ('created', models.DateTimeField(auto_now_add=True)), - ('deleted', models.BooleanField(default=False, verbose_name='deleted')), - ('locked', models.BooleanField(default=False, verbose_name='locked')), - ('content', models.TextField(blank=True, verbose_name='article contents')), - ('title', models.CharField(max_length=512, verbose_name='article title', help_text='Each revision contains a title field that must be filled out, even if the title has not changed')), - ('article', models.ForeignKey(to='wiki.Article', verbose_name='article', on_delete=models.CASCADE)), - ('previous_revision', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, blank=True, to='wiki.ArticleRevision')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, verbose_name='user')), + ( + "id", + models.AutoField( + serialize=False, + primary_key=True, + auto_created=True, + verbose_name="ID", + ), + ), + ( + "revision_number", + models.IntegerField(verbose_name="revision number", editable=False), + ), + ("user_message", models.TextField(blank=True)), + ("automatic_log", models.TextField(blank=True, editable=False)), + ( + "ip_address", + IPAddressField( + null=True, verbose_name="IP address", blank=True, editable=False + ), + ), + ("modified", models.DateTimeField(auto_now=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("deleted", models.BooleanField(default=False, verbose_name="deleted")), + ("locked", models.BooleanField(default=False, verbose_name="locked")), + ( + "content", + models.TextField(blank=True, verbose_name="article contents"), + ), + ( + "title", + models.CharField( + max_length=512, + verbose_name="article title", + help_text="Each revision contains a title field that must be filled out, even if the title has not changed", + ), + ), + ( + "article", + models.ForeignKey( + to="wiki.Article", + verbose_name="article", + on_delete=models.CASCADE, + ), + ), + ( + "previous_revision", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + blank=True, + to="wiki.ArticleRevision", + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + blank=True, + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), + ), ], - options={ - 'get_latest_by': 'revision_number', - 'ordering': ('created',), - }, + options={"get_latest_by": "revision_number", "ordering": ("created",),}, bases=(models.Model,), ), migrations.CreateModel( - name='ReusablePlugin', + name="ReusablePlugin", fields=[ - ('articleplugin_ptr', models.OneToOneField(primary_key=True, parent_link=True, to='wiki.ArticlePlugin', serialize=False, auto_created=True, on_delete=models.CASCADE)), - ('articles', models.ManyToManyField(related_name='shared_plugins_set', to='wiki.Article')), + ( + "articleplugin_ptr", + models.OneToOneField( + primary_key=True, + parent_link=True, + to="wiki.ArticlePlugin", + serialize=False, + auto_created=True, + on_delete=models.CASCADE, + ), + ), + ( + "articles", + models.ManyToManyField( + related_name="shared_plugins_set", to="wiki.Article" + ), + ), ], - options={ - }, - bases=('wiki.articleplugin',), + options={}, + bases=("wiki.articleplugin",), ), migrations.CreateModel( - name='RevisionPlugin', + name="RevisionPlugin", fields=[ - ('articleplugin_ptr', models.OneToOneField(primary_key=True, parent_link=True, to='wiki.ArticlePlugin', serialize=False, auto_created=True, on_delete=models.CASCADE)), + ( + "articleplugin_ptr", + models.OneToOneField( + primary_key=True, + parent_link=True, + to="wiki.ArticlePlugin", + serialize=False, + auto_created=True, + on_delete=models.CASCADE, + ), + ), ], - options={ - }, - bases=('wiki.articleplugin',), + options={}, + bases=("wiki.articleplugin",), ), migrations.CreateModel( - name='RevisionPluginRevision', + name="RevisionPluginRevision", fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('revision_number', models.IntegerField(verbose_name='revision number', editable=False)), - ('user_message', models.TextField(blank=True)), - ('automatic_log', models.TextField(blank=True, editable=False)), - ('ip_address', IPAddressField(null=True, verbose_name='IP address', blank=True, editable=False)), - ('modified', models.DateTimeField(auto_now=True)), - ('created', models.DateTimeField(auto_now_add=True)), - ('deleted', models.BooleanField(default=False, verbose_name='deleted')), - ('locked', models.BooleanField(default=False, verbose_name='locked')), - ('plugin', models.ForeignKey(related_name='revision_set', to='wiki.RevisionPlugin', on_delete=models.CASCADE)), - ('previous_revision', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, blank=True, to='wiki.RevisionPluginRevision')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, verbose_name='user')), + ( + "id", + models.AutoField( + serialize=False, + primary_key=True, + auto_created=True, + verbose_name="ID", + ), + ), + ( + "revision_number", + models.IntegerField(verbose_name="revision number", editable=False), + ), + ("user_message", models.TextField(blank=True)), + ("automatic_log", models.TextField(blank=True, editable=False)), + ( + "ip_address", + IPAddressField( + null=True, verbose_name="IP address", blank=True, editable=False + ), + ), + ("modified", models.DateTimeField(auto_now=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("deleted", models.BooleanField(default=False, verbose_name="deleted")), + ("locked", models.BooleanField(default=False, verbose_name="locked")), + ( + "plugin", + models.ForeignKey( + related_name="revision_set", + to="wiki.RevisionPlugin", + on_delete=models.CASCADE, + ), + ), + ( + "previous_revision", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + blank=True, + to="wiki.RevisionPluginRevision", + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + blank=True, + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), + ), ], - options={ - 'get_latest_by': 'revision_number', - 'ordering': ('-created',), - }, + options={"get_latest_by": "revision_number", "ordering": ("-created",),}, bases=(models.Model,), ), migrations.CreateModel( - name='SimplePlugin', + name="SimplePlugin", fields=[ - ('articleplugin_ptr', models.OneToOneField(primary_key=True, parent_link=True, to='wiki.ArticlePlugin', serialize=False, auto_created=True, on_delete=models.CASCADE)), - ('article_revision', models.ForeignKey(to='wiki.ArticleRevision', on_delete=models.CASCADE)), + ( + "articleplugin_ptr", + models.OneToOneField( + primary_key=True, + parent_link=True, + to="wiki.ArticlePlugin", + serialize=False, + auto_created=True, + on_delete=models.CASCADE, + ), + ), + ( + "article_revision", + models.ForeignKey( + to="wiki.ArticleRevision", on_delete=models.CASCADE + ), + ), ], - options={ - }, - bases=('wiki.articleplugin',), + options={}, + bases=("wiki.articleplugin",), ), migrations.CreateModel( - name='URLPath', + name="URLPath", fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('slug', models.SlugField(null=True, blank=True, verbose_name='slug')), - ('lft', models.PositiveIntegerField(db_index=True, editable=False)), - ('rght', models.PositiveIntegerField(db_index=True, editable=False)), - ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), - ('level', models.PositiveIntegerField(db_index=True, editable=False)), - ('article', models.ForeignKey(help_text='This field is automatically updated, but you need to populate it when creating a new URL path.', on_delete=django.db.models.deletion.CASCADE, to='wiki.Article', verbose_name='article')), - ('parent', mptt.fields.TreeForeignKey(blank=True, help_text='Position of URL path in the tree.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='wiki.URLPath')), - ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')), + ( + "id", + models.AutoField( + serialize=False, + primary_key=True, + auto_created=True, + verbose_name="ID", + ), + ), + ("slug", models.SlugField(null=True, blank=True, verbose_name="slug")), + ("lft", models.PositiveIntegerField(db_index=True, editable=False)), + ("rght", models.PositiveIntegerField(db_index=True, editable=False)), + ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)), + ("level", models.PositiveIntegerField(db_index=True, editable=False)), + ( + "article", + models.ForeignKey( + help_text="This field is automatically updated, but you need to populate it when creating a new URL path.", + on_delete=django.db.models.deletion.CASCADE, + to="wiki.Article", + verbose_name="article", + ), + ), + ( + "parent", + mptt.fields.TreeForeignKey( + blank=True, + help_text="Position of URL path in the tree.", + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="children", + to="wiki.URLPath", + ), + ), + ( + "site", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="sites.Site" + ), + ), ], - options={ - 'verbose_name_plural': 'URL paths', - 'verbose_name': 'URL path', - }, + options={"verbose_name_plural": "URL paths", "verbose_name": "URL path",}, bases=(models.Model,), ), migrations.AlterUniqueTogether( - name='urlpath', - unique_together=set([('site', 'parent', 'slug')]), + name="urlpath", unique_together=set([("site", "parent", "slug")]), ), migrations.AddField( - model_name='revisionplugin', - name='current_revision', - field=models.OneToOneField(related_name='plugin_set', null=True, help_text='The revision being displayed for this plugin. If you need to do a roll-back, simply change the value of this field.', blank=True, to='wiki.RevisionPluginRevision', verbose_name='current revision', on_delete=models.CASCADE), + model_name="revisionplugin", + name="current_revision", + field=models.OneToOneField( + related_name="plugin_set", + null=True, + help_text="The revision being displayed for this plugin. If you need to do a roll-back, simply change the value of this field.", + blank=True, + to="wiki.RevisionPluginRevision", + verbose_name="current revision", + on_delete=models.CASCADE, + ), preserve_default=True, ), migrations.AlterUniqueTogether( - name='articlerevision', - unique_together=set([('article', 'revision_number')]), + name="articlerevision", + unique_together=set([("article", "revision_number")]), ), migrations.AddField( - model_name='articleplugin', - name='article', - field=models.ForeignKey(to='wiki.Article', verbose_name='article', on_delete=models.CASCADE), + model_name="articleplugin", + name="article", + field=models.ForeignKey( + to="wiki.Article", verbose_name="article", on_delete=models.CASCADE + ), preserve_default=True, ), migrations.AlterUniqueTogether( - name='articleforobject', - unique_together=set([('content_type', 'object_id')]), + name="articleforobject", + unique_together=set([("content_type", "object_id")]), ), migrations.AddField( - model_name='article', - name='current_revision', - field=models.OneToOneField(related_name='current_set', null=True, help_text='The revision being displayed for this article. If you need to do a roll-back, simply change the value of this field.', blank=True, to='wiki.ArticleRevision', verbose_name='current revision', on_delete=models.CASCADE), + model_name="article", + name="current_revision", + field=models.OneToOneField( + related_name="current_set", + null=True, + help_text="The revision being displayed for this article. If you need to do a roll-back, simply change the value of this field.", + blank=True, + to="wiki.ArticleRevision", + verbose_name="current revision", + on_delete=models.CASCADE, + ), preserve_default=True, ), migrations.AddField( - model_name='article', - name='group', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, help_text='Like in a UNIX file system, permissions can be given to a user according to group membership. Groups are handled through the Django auth system.', blank=True, to=GROUP_MODEL, verbose_name='group'), + model_name="article", + name="group", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + help_text="Like in a UNIX file system, permissions can be given to a user according to group membership. Groups are handled through the Django auth system.", + blank=True, + to=GROUP_MODEL, + verbose_name="group", + ), preserve_default=True, ), migrations.AddField( - model_name='article', - name='owner', - field=models.ForeignKey(related_name='owned_articles', null=True, on_delete=django.db.models.deletion.SET_NULL, help_text='The owner of the article, usually the creator. The owner always has both read and write access.', blank=True, to=settings.AUTH_USER_MODEL, verbose_name='owner'), + model_name="article", + name="owner", + field=models.ForeignKey( + related_name="owned_articles", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + help_text="The owner of the article, usually the creator. The owner always has both read and write access.", + blank=True, + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), preserve_default=True, ), ] diff --git a/src/wiki/migrations/0002_urlpath_moved_to.py b/src/wiki/migrations/0002_urlpath_moved_to.py index a1a97da3..42046581 100644 --- a/src/wiki/migrations/0002_urlpath_moved_to.py +++ b/src/wiki/migrations/0002_urlpath_moved_to.py @@ -7,13 +7,21 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('wiki', '0001_initial'), + ("wiki", "0001_initial"), ] operations = [ migrations.AddField( - model_name='urlpath', - name='moved_to', - field=mptt.fields.TreeForeignKey(blank=True, help_text='Article path was moved to this location', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='moved_from', to='wiki.URLPath', verbose_name='Moved to'), + model_name="urlpath", + name="moved_to", + field=mptt.fields.TreeForeignKey( + blank=True, + help_text="Article path was moved to this location", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="moved_from", + to="wiki.URLPath", + verbose_name="Moved to", + ), ), ] diff --git a/src/wiki/models/__init__.py b/src/wiki/models/__init__.py index e2a31b84..8311014d 100644 --- a/src/wiki/models/__init__.py +++ b/src/wiki/models/__init__.py @@ -19,17 +19,17 @@ def reverse(*args, **kwargs): return the result of calling reverse._transform_url(reversed_url) for every url in the wiki namespace. """ - if isinstance(args[0], str) and args[0].startswith('wiki:'): - url_kwargs = kwargs.get('kwargs', {}) - path = url_kwargs.get('path', False) + if isinstance(args[0], str) and args[0].startswith("wiki:"): + url_kwargs = kwargs.get("kwargs", {}) + path = url_kwargs.get("path", False) # If a path is supplied then discard the article_id if path is not False: - url_kwargs.pop('article_id', None) - url_kwargs['path'] = path - kwargs['kwargs'] = url_kwargs + url_kwargs.pop("article_id", None) + url_kwargs["path"] = path + kwargs["kwargs"] = url_kwargs url = original_django_reverse(*args, **kwargs) - if hasattr(reverse, '_transform_url'): + if hasattr(reverse, "_transform_url"): url = reverse._transform_url(url) else: url = original_django_reverse(*args, **kwargs) diff --git a/src/wiki/models/article.py b/src/wiki/models/article.py index fbf55264..43f8f60f 100644 --- a/src/wiki/models/article.py +++ b/src/wiki/models/article.py @@ -17,8 +17,10 @@ from wiki.core.markdown import article_markdown from wiki.decorators import disable_signal_for_loaddata __all__ = [ - 'Article', 'ArticleForObject', 'ArticleRevision', - 'BaseRevisionMixin', + "Article", + "ArticleForObject", + "ArticleRevision", + "BaseRevisionMixin", ] @@ -27,47 +29,55 @@ class Article(models.Model): objects = managers.ArticleManager() current_revision = models.OneToOneField( - 'ArticleRevision', verbose_name=_('current revision'), - blank=True, null=True, related_name='current_set', + "ArticleRevision", + verbose_name=_("current revision"), + blank=True, + null=True, + related_name="current_set", on_delete=models.CASCADE, help_text=_( - 'The revision being displayed for this article. If you need to do a roll-back, simply change the value of this field.'),) - - created = models.DateTimeField( - auto_now_add=True, - verbose_name=_('created'), + "The revision being displayed for this article. If you need to do a roll-back, simply change the value of this field." + ), ) + + created = models.DateTimeField(auto_now_add=True, verbose_name=_("created"),) modified = models.DateTimeField( auto_now=True, - verbose_name=_('modified'), - help_text=_('Article properties last modified')) + verbose_name=_("modified"), + help_text=_("Article properties last modified"), + ) owner = models.ForeignKey( - django_settings.AUTH_USER_MODEL, verbose_name=_('owner'), - blank=True, null=True, related_name='owned_articles', + django_settings.AUTH_USER_MODEL, + verbose_name=_("owner"), + blank=True, + null=True, + related_name="owned_articles", help_text=_( - 'The owner of the article, usually the creator. The owner always has both read and write access.'), - on_delete=models.SET_NULL) + "The owner of the article, usually the creator. The owner always has both read and write access." + ), + on_delete=models.SET_NULL, + ) group = models.ForeignKey( - settings.GROUP_MODEL, verbose_name=_('group'), - blank=True, null=True, + settings.GROUP_MODEL, + verbose_name=_("group"), + blank=True, + null=True, help_text=_( - 'Like in a UNIX file system, permissions can be given to a user according to group membership. Groups are handled through the Django auth system.'), - on_delete=models.SET_NULL) + "Like in a UNIX file system, permissions can be given to a user according to group membership. Groups are handled through the Django auth system." + ), + on_delete=models.SET_NULL, + ) - group_read = models.BooleanField( - default=True, - verbose_name=_('group read access')) + group_read = models.BooleanField(default=True, verbose_name=_("group read access")) group_write = models.BooleanField( - default=True, - verbose_name=_('group write access')) - other_read = models.BooleanField( - default=True, - verbose_name=_('others read access')) + default=True, verbose_name=_("group write access") + ) + other_read = models.BooleanField(default=True, verbose_name=_("others read access")) other_write = models.BooleanField( - default=True, - verbose_name=_('others write access')) + default=True, verbose_name=_("others write access") + ) # PERMISSIONS def can_read(self, user): @@ -100,11 +110,14 @@ class Article(models.Model): cnt = 0 for obj in self.articleforobject_set.filter(is_mptt=True): if user_can_read: - objects = obj.content_object.get_children().filter( - **kwargs).can_read(user_can_read) + objects = ( + obj.content_object.get_children() + .filter(**kwargs) + .can_read(user_can_read) + ) else: objects = obj.content_object.get_children().filter(**kwargs) - for child in objects.order_by('articles__article__current_revision__title'): + for child in objects.order_by("articles__article__current_revision__title"): cnt += 1 if max_num and cnt > max_num: return @@ -140,9 +153,10 @@ class Article(models.Model): revision. """ assert self.id or save, ( - 'Article.add_revision: Sorry, you cannot add a' - 'revision to an article that has not been saved ' - 'without using save=True') + "Article.add_revision: Sorry, you cannot add a" + "revision to an article that has not been saved " + "without using save=True" + ) if not self.id: self.save() revisions = self.articlerevision_set.all() @@ -170,14 +184,13 @@ class Article(models.Model): @classmethod def get_for_object(cls, obj): return ArticleForObject.objects.get( - object_id=obj.id, - content_type=ContentType.objects.get_for_model(obj), + object_id=obj.id, content_type=ContentType.objects.get_for_model(obj), ).article def __str__(self): if self.current_revision: return self.current_revision.title - obj_name = _('Article without content (%(id)d)') % {'id': self.id} + obj_name = _("Article without content (%(id)d)") % {"id": self.id} return str(obj_name) class Meta: @@ -194,10 +207,10 @@ class Article(models.Model): content = preview_content else: content = self.current_revision.content - return mark_safe(article_markdown( - content, self, - preview=preview_content is not None, - user=user) + return mark_safe( + article_markdown( + content, self, preview=preview_content is not None, user=user + ) ) def get_cache_key(self): @@ -205,14 +218,14 @@ class Article(models.Model): lang = translation.get_language() return "wiki:article:{id}:{lang}".format( - id=self.current_revision.id if self.current_revision else self.id, - lang=lang) + id=self.current_revision.id if self.current_revision else self.id, lang=lang + ) def get_cache_content_key(self, user=None): """Returns per-article-user cache key.""" return "{key}:{user}".format( - key=self.get_cache_key(), - user=user.get_username() if user else "") + key=self.get_cache_key(), user=user.get_username() if user else "" + ) def get_cached_content(self, user=None): """Returns cached version of rendered article. @@ -247,25 +260,26 @@ class Article(models.Model): def get_url_kwargs(self): urlpaths = self.urlpath_set.all() if urlpaths.exists(): - return {'path': urlpaths[0].path} - return {'article_id': self.id} + return {"path": urlpaths[0].path} + return {"article_id": self.id} def get_absolute_url(self): - return reverse('wiki:get', kwargs=self.get_url_kwargs()) + return reverse("wiki:get", kwargs=self.get_url_kwargs()) class ArticleForObject(models.Model): objects = managers.ArticleFkManager() - article = models.ForeignKey('Article', on_delete=models.CASCADE) + article = models.ForeignKey("Article", on_delete=models.CASCADE) # Same as django.contrib.comments content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, - verbose_name=_('content type'), - related_name="content_type_set_for_%(class)s") - object_id = models.PositiveIntegerField(_('object ID')) + verbose_name=_("content type"), + related_name="content_type_set_for_%(class)s", + ) + object_id = models.PositiveIntegerField(_("object ID")) content_object = GenericForeignKey("content_type", "object_id") is_mptt = models.BooleanField(default=False, editable=False) @@ -274,10 +288,10 @@ class ArticleForObject(models.Model): return str(self.article) class Meta: - verbose_name = _('Article for object') - verbose_name_plural = _('Articles for object') + verbose_name = _("Article for object") + verbose_name_plural = _("Articles for object") # Do not allow several objects - unique_together = ('content_type', 'object_id') + unique_together = ("content_type", "object_id") class BaseRevisionMixin(models.Model): @@ -286,47 +300,41 @@ class BaseRevisionMixin(models.Model): core model methods but respect the inheritor's freedom to do so itself.""" revision_number = models.IntegerField( - editable=False, - verbose_name=_('revision number')) + editable=False, verbose_name=_("revision number") + ) user_message = models.TextField(blank=True,) automatic_log = models.TextField(blank=True, editable=False,) - ip_address = IPAddressField( - _('IP address'), + ip_address = IPAddressField(_("IP address"), blank=True, null=True, editable=False) + user = models.ForeignKey( + django_settings.AUTH_USER_MODEL, + verbose_name=_("user"), blank=True, null=True, - editable=False) - user = models.ForeignKey(django_settings.AUTH_USER_MODEL, verbose_name=_('user'), - blank=True, null=True, - on_delete=models.SET_NULL) + on_delete=models.SET_NULL, + ) modified = models.DateTimeField(auto_now=True) created = models.DateTimeField(auto_now_add=True) previous_revision = models.ForeignKey( - 'self', blank=True, null=True, on_delete=models.SET_NULL + "self", blank=True, null=True, on_delete=models.SET_NULL ) # NOTE! The semantics of these fields are not related to the revision itself # but the actual related object. If the latest revision says "deleted=True" then # the related object should be regarded as deleted. - deleted = models.BooleanField( - verbose_name=_('deleted'), - default=False, - ) - locked = models.BooleanField( - verbose_name=_('locked'), - default=False, - ) + deleted = models.BooleanField(verbose_name=_("deleted"), default=False,) + locked = models.BooleanField(verbose_name=_("locked"), default=False,) def set_from_request(self, request): if request.user.is_authenticated: self.user = request.user if settings.LOG_IPS_USERS: - self.ip_address = request.META.get('REMOTE_ADDR', None) + self.ip_address = request.META.get("REMOTE_ADDR", None) elif settings.LOG_IPS_ANONYMOUS: - self.ip_address = request.META.get('REMOTE_ADDR', None) + self.ip_address = request.META.get("REMOTE_ADDR", None) def inherit_predecessor(self, predecessor): """ @@ -353,19 +361,24 @@ class ArticleRevision(BaseRevisionMixin, models.Model): objects = managers.ArticleFkManager() - article = models.ForeignKey('Article', on_delete=models.CASCADE, - verbose_name=_('article')) + article = models.ForeignKey( + "Article", on_delete=models.CASCADE, verbose_name=_("article") + ) # This is where the content goes, with whatever markup language is used - content = models.TextField(blank=True, verbose_name=_('article contents')) + content = models.TextField(blank=True, verbose_name=_("article contents")) # This title is automatically set from either the article's title or # the last used revision... title = models.CharField( - max_length=512, verbose_name=_('article title'), - null=False, blank=False, + max_length=512, + verbose_name=_("article title"), + null=False, + blank=False, help_text=_( - 'Each revision contains a title field that must be filled out, even if the title has not changed')) + "Each revision contains a title field that must be filled out, even if the title has not changed" + ), + ) # TODO: # Allow a revision to redirect to another *article*. This @@ -382,7 +395,7 @@ class ArticleRevision(BaseRevisionMixin, models.Model): # Enforce DOS line endings \r\n. It is the standard for web browsers, # but when revisions are created programatically, they might # have UNIX line endings \n instead. - self.content = self.content.replace('\r', '').replace('\n', '\r\n') + self.content = self.content.replace("\r", "").replace("\n", "\r\n") def inherit_predecessor(self, article): """ @@ -397,9 +410,9 @@ class ArticleRevision(BaseRevisionMixin, models.Model): self.locked = predecessor.locked class Meta: - get_latest_by = 'revision_number' - ordering = ('created',) - unique_together = ('article', 'revision_number') + get_latest_by = "revision_number" + ordering = ("created",) + unique_together = ("article", "revision_number") ###################################################### @@ -426,13 +439,13 @@ def on_article_delete_clear_cache(instance, **kwargs): @disable_signal_for_loaddata def on_article_revision_pre_save(**kwargs): - instance = kwargs['instance'] - if kwargs.get('created', False): + instance = kwargs["instance"] + if kwargs.get("created", False): revision_changed = ( - not instance.previous_revision and - instance.article and - instance.article.current_revision and - instance.article.current_revision != instance + not instance.previous_revision + and instance.article + and instance.article.current_revision + and instance.article.current_revision != instance ) if revision_changed: instance.previous_revision = instance.article.current_revision @@ -448,7 +461,7 @@ def on_article_revision_pre_save(**kwargs): @disable_signal_for_loaddata def on_article_revision_post_save(**kwargs): - instance = kwargs['instance'] + instance = kwargs["instance"] if not instance.article.current_revision: # If I'm saved from Django admin, then article.current_revision is # me! diff --git a/src/wiki/models/pluginbase.py b/src/wiki/models/pluginbase.py index 98a8f459..cd6b7aac 100644 --- a/src/wiki/models/pluginbase.py +++ b/src/wiki/models/pluginbase.py @@ -26,10 +26,12 @@ from wiki.decorators import disable_signal_for_loaddata from .article import ArticleRevision, BaseRevisionMixin __all__ = [ - 'ArticlePlugin', - 'SimplePlugin', 'SimplePluginCreateError', - 'ReusablePlugin', - 'RevisionPlugin', 'RevisionPluginRevision', + "ArticlePlugin", + "SimplePlugin", + "SimplePluginCreateError", + "ReusablePlugin", + "RevisionPlugin", + "RevisionPluginRevision", ] @@ -40,8 +42,9 @@ class ArticlePlugin(models.Model): clean. Furthermore, it's possible to list all plugins and maintain generic properties in the future...""" - article = models.ForeignKey('wiki.Article', on_delete=models.CASCADE, - verbose_name=_("article")) + article = models.ForeignKey( + "wiki.Article", on_delete=models.CASCADE, verbose_name=_("article") + ) deleted = models.BooleanField(default=False) @@ -79,18 +82,16 @@ class ReusablePlugin(ArticlePlugin): You might have to override the permission methods (can_read, can_write etc.) if you have certain needs for logic in your reusable plugin. """ + # The article on which the plugin was originally created. # Used to apply permissions. ArticlePlugin.article.on_delete = models.SET_NULL - ArticlePlugin.article.verbose_name = _('original article') - ArticlePlugin.article.help_text = _( - 'Permissions are inherited from this article') + ArticlePlugin.article.verbose_name = _("original article") + ArticlePlugin.article.help_text = _("Permissions are inherited from this article") ArticlePlugin.article.null = True ArticlePlugin.article.blank = True - articles = models.ManyToManyField( - 'wiki.Article', - related_name='shared_plugins_set') + articles = models.ManyToManyField("wiki.Article", related_name="shared_plugins_set") # Since the article relation may be None, we have to check for this # before handling permissions.... @@ -130,17 +131,17 @@ class SimplePlugin(ArticlePlugin): YourPlugin(article=article_instance, ...) or YourPlugin.objects.create(article=article_instance, ...) """ + # The article revision that this plugin is attached to article_revision = models.ForeignKey( - 'wiki.ArticleRevision', - on_delete=models.CASCADE) + "wiki.ArticleRevision", on_delete=models.CASCADE + ) def __init__(self, *args, **kwargs): - article = kwargs.pop('article', None) + article = kwargs.pop("article", None) super().__init__(*args, **kwargs) if not self.pk and not article: - raise SimplePluginCreateError( - "Keyword argument 'article' expected.") + raise SimplePluginCreateError("Keyword argument 'article' expected.") elif self.pk: self.article = self.article_revision.article else: @@ -159,17 +160,19 @@ class RevisionPlugin(ArticlePlugin): This kind of plugin is not attached to article plugins so rolling articles back and forth does not affect it. """ + # The current revision of this plugin, if any! current_revision = models.OneToOneField( - 'RevisionPluginRevision', - verbose_name=_('current revision'), + "RevisionPluginRevision", + verbose_name=_("current revision"), blank=True, null=True, on_delete=models.CASCADE, - related_name='plugin_set', + related_name="plugin_set", help_text=_( - 'The revision being displayed for this plugin. ' - 'If you need to do a roll-back, simply change the value of this field.'), + "The revision being displayed for this plugin. " + "If you need to do a roll-back, simply change the value of this field." + ), ) def add_revision(self, new_revision, save=True): @@ -178,15 +181,15 @@ class RevisionPlugin(ArticlePlugin): revision. """ assert self.id or save, ( - 'RevisionPluginRevision.add_revision: Sorry, you cannot add a' - 'revision to a plugin that has not been saved ' - 'without using save=True') + "RevisionPluginRevision.add_revision: Sorry, you cannot add a" + "revision to a plugin that has not been saved " + "without using save=True" + ) if not self.id: self.save() revisions = self.revision_set.all() try: - new_revision.revision_number = revisions.latest( - ).revision_number + 1 + new_revision.revision_number = revisions.latest().revision_number + 1 except RevisionPluginRevision.DoesNotExist: new_revision.revision_number = 0 new_revision.plugin = self @@ -207,13 +210,15 @@ class RevisionPluginRevision(BaseRevisionMixin, models.Model): (this class is very much copied from wiki.models.article.ArticleRevision """ - plugin = models.ForeignKey(RevisionPlugin, on_delete=models.CASCADE, related_name='revision_set') + plugin = models.ForeignKey( + RevisionPlugin, on_delete=models.CASCADE, related_name="revision_set" + ) class Meta: # Override this setting with app_label = '' in your extended model # if it lives outside the wiki app. - get_latest_by = 'revision_number' - ordering = ('-created',) + get_latest_by = "revision_number" + ordering = ("-created",) ###################################################### @@ -230,22 +235,23 @@ class RevisionPluginRevision(BaseRevisionMixin, models.Model): def update_simple_plugins(**kwargs): """Every time a new article revision is created, we update all active plugins to match this article revision""" - instance = kwargs['instance'] - if kwargs.get('created', False): + instance = kwargs["instance"] + if kwargs.get("created", False): p_revisions = SimplePlugin.objects.filter( - article=instance.article, - deleted=False) + article=instance.article, deleted=False + ) # TODO: This was breaking things. SimplePlugin doesn't have a revision? p_revisions.update(article_revision=instance) @disable_signal_for_loaddata def on_simple_plugins_pre_save(**kwargs): - instance = kwargs['instance'] - if kwargs.get('created', False): + instance = kwargs["instance"] + if kwargs.get("created", False): if not instance.article.current_revision: raise SimplePluginCreateError( - "Article does not have a current_revision set.") + "Article does not have a current_revision set." + ) new_revision = ArticleRevision() new_revision.inherit_predecessor(instance.article) new_revision.automatic_log = instance.get_logmessage() @@ -256,7 +262,7 @@ def on_simple_plugins_pre_save(**kwargs): @disable_signal_for_loaddata def on_article_plugin_post_save(**kwargs): - articleplugin = kwargs['instance'] + articleplugin = kwargs["instance"] articleplugin.article.clear_cache() @@ -264,7 +270,7 @@ def on_article_plugin_post_save(**kwargs): def on_reusable_plugin_pre_save(**kwargs): # Automatically make the original article the first one in the added # set - instance = kwargs['instance'] + instance = kwargs["instance"] if not instance.article: articles = instance.articles.all() if articles.exists(): @@ -275,7 +281,7 @@ def on_reusable_plugin_pre_save(**kwargs): def on_revision_plugin_revision_post_save(**kwargs): # Automatically make the original article the first one in the added # set - instance = kwargs['instance'] + instance = kwargs["instance"] if not instance.plugin.current_revision: # If I'm saved from Django admin, then plugin.current_revision is # me! @@ -288,13 +294,13 @@ def on_revision_plugin_revision_post_save(**kwargs): @disable_signal_for_loaddata def on_revision_plugin_revision_pre_save(**kwargs): - instance = kwargs['instance'] - if kwargs.get('created', False): + instance = kwargs["instance"] + if kwargs.get("created", False): update_previous_revision = ( - not instance.previous_revision and - instance.plugin and - instance.plugin.current_revision and - instance.plugin.current_revision != instance + not instance.previous_revision + and instance.plugin + and instance.plugin.current_revision + and instance.plugin.current_revision != instance ) if update_previous_revision: instance.previous_revision = instance.plugin.current_revision @@ -309,7 +315,7 @@ def on_revision_plugin_revision_pre_save(**kwargs): @disable_signal_for_loaddata def on_reusable_plugin_post_save(**kwargs): - reusableplugin = kwargs['instance'] + reusableplugin = kwargs["instance"] for article in reusableplugin.articles.all(): article.clear_cache() diff --git a/src/wiki/models/urlpath.py b/src/wiki/models/urlpath.py index cb739825..4a8422ab 100644 --- a/src/wiki/models/urlpath.py +++ b/src/wiki/models/urlpath.py @@ -18,7 +18,7 @@ from wiki.decorators import disable_signal_for_loaddata from wiki.models.article import Article, ArticleForObject, ArticleRevision __all__ = [ - 'URLPath', + "URLPath", ] @@ -31,6 +31,7 @@ class URLPath(MPTTModel): Strategy: Very few fields go here, as most has to be managed through an article's revision. As a side-effect, the URL resolution remains slim and swift. """ + # Tells django-wiki that permissions from a this object's article # should be inherited to children's articles. In this case, it's a static # property.. but you can also use a BooleanField. @@ -44,8 +45,8 @@ class URLPath(MPTTModel): articles = GenericRelation( ArticleForObject, - content_type_field='content_type', - object_id_field='object_id', + content_type_field="content_type", + object_id_field="object_id", ) # Do NOT modify this field - it is updated with signals whenever @@ -53,34 +54,35 @@ class URLPath(MPTTModel): article = models.ForeignKey( Article, on_delete=models.CASCADE, - verbose_name=_('article'), + verbose_name=_("article"), help_text=_( "This field is automatically updated, but you need to populate " "it when creating a new URL path." - ) + ), ) SLUG_MAX_LENGTH = 50 - slug = models.SlugField(verbose_name=_('slug'), null=True, blank=True, - max_length=SLUG_MAX_LENGTH) + slug = models.SlugField( + verbose_name=_("slug"), null=True, blank=True, max_length=SLUG_MAX_LENGTH + ) site = models.ForeignKey(Site, on_delete=models.CASCADE) parent = TreeForeignKey( - 'self', + "self", null=True, blank=True, on_delete=models.CASCADE, - related_name='children', - help_text=_("Position of URL path in the tree.") + related_name="children", + help_text=_("Position of URL path in the tree."), ) moved_to = TreeForeignKey( - 'self', + "self", verbose_name=_("Moved to"), help_text=_("Article path was moved to this location"), null=True, blank=True, on_delete=models.SET_NULL, - related_name='moved_from' + related_name="moved_from", ) def __cached_ancestors(self): @@ -99,8 +101,7 @@ class URLPath(MPTTModel): if not self.pk or not self.get_ancestors().exists(): self._cached_ancestors = [] if not hasattr(self, "_cached_ancestors"): - self._cached_ancestors = list( - self.get_ancestors().select_related_common()) + self._cached_ancestors = list(self.get_ancestors().select_related_common()) return self._cached_ancestors @@ -108,8 +109,7 @@ class URLPath(MPTTModel): self._cached_ancestors = ancestors # Python 2.5 compatible property constructor - cached_ancestors = property(__cached_ancestors, - __cached_ancestors_setter) + cached_ancestors = property(__cached_ancestors, __cached_ancestors_setter) def set_cached_ancestors_from_parent(self, parent): self.cached_ancestors = parent.cached_ancestors + [parent] @@ -121,10 +121,7 @@ class URLPath(MPTTModel): # All ancestors except roots ancestors = list( - filter( - lambda ancestor: ancestor.parent is not None, - self.cached_ancestors - ) + filter(lambda ancestor: ancestor.parent is not None, self.cached_ancestors) ) slugs = [obj.slug if obj.slug else "" for obj in ancestors + [self]] @@ -144,8 +141,7 @@ class URLPath(MPTTModel): @transaction.atomic def _delete_subtree(self): - for descendant in self.get_descendants( - include_self=True).order_by("-level"): + for descendant in self.get_descendants(include_self=True).order_by("-level"): descendant.article.delete() def delete_subtree(self): @@ -163,13 +159,9 @@ class URLPath(MPTTModel): # to get the result out anyway. This only takes one sql query no_paths = len(root_nodes) if no_paths == 0: - raise NoRootURL( - "You need to create a root article on site '%s'" % - site) + raise NoRootURL("You need to create a root article on site '%s'" % site) if no_paths > 1: - raise MultipleRootURLs( - "Somehow you have multiple roots on %s" % - site) + raise MultipleRootURLs("Somehow you have multiple roots on %s" % site) return root_nodes[0] class MPTTMeta: @@ -180,29 +172,28 @@ class URLPath(MPTTModel): return path if path else gettext("(root)") def delete(self, *args, **kwargs): - assert not (self.parent and self.get_children() - ), "You cannot delete a root article with children." + assert not ( + self.parent and self.get_children() + ), "You cannot delete a root article with children." super().delete(*args, **kwargs) class Meta: - verbose_name = _('URL path') - verbose_name_plural = _('URL paths') - unique_together = ('site', 'parent', 'slug') + verbose_name = _("URL path") + verbose_name_plural = _("URL paths") + unique_together = ("site", "parent", "slug") def clean(self, *args, **kwargs): if self.slug and not self.parent: raise ValidationError( - _('Sorry but you cannot have a root article with a slug.')) + _("Sorry but you cannot have a root article with a slug.") + ) if not self.slug and self.parent: - raise ValidationError( - _('A non-root note must always have a slug.')) + raise ValidationError(_("A non-root note must always have a slug.")) if not self.parent: - if URLPath.objects.root_nodes().filter( - site=self.site).exclude( - id=self.id): + if URLPath.objects.root_nodes().filter(site=self.site).exclude(id=self.id): raise ValidationError( - _('There is already a root node on %s') % - self.site) + _("There is already a root node on %s") % self.site + ) @classmethod def get_by_path(cls, path, select_related=False): @@ -222,18 +213,18 @@ class URLPath(MPTTModel): if not path: return cls.root() - slugs = path.split('/') + slugs = path.split("/") level = 1 parent = cls.root() for slug in slugs: if settings.URL_CASE_SENSITIVE: - child = parent.get_children().select_related_common().get( - slug=slug) + child = parent.get_children().select_related_common().get(slug=slug) child.cached_ancestors = parent.cached_ancestors + [parent] parent = child else: - child = parent.get_children().select_related_common().get( - slug__iexact=slug) + child = ( + parent.get_children().select_related_common().get(slug__iexact=slug) + ) child.cached_ancestors = parent.cached_ancestors + [parent] parent = child level += 1 @@ -241,7 +232,7 @@ class URLPath(MPTTModel): return parent def get_absolute_url(self): - return reverse('wiki:get', kwargs={'path': self.path}) + return reverse("wiki:get", kwargs={"path": self.path}) @classmethod def create_root(cls, site=None, title="Root", request=None, **kwargs): @@ -264,15 +255,16 @@ class URLPath(MPTTModel): @classmethod @transaction.atomic def create_urlpath( - cls, - parent, - slug, - site=None, - title="Root", - article_kwargs={}, - request=None, - article_w_permissions=None, - **revision_kwargs): + cls, + parent, + slug, + site=None, + title="Root", + article_kwargs={}, + request=None, + article_w_permissions=None, + **revision_kwargs + ): """ Utility function: Creates a new urlpath with an article and a new revision for the @@ -283,27 +275,18 @@ class URLPath(MPTTModel): if not site: site = Site.objects.get_current() article = Article(**article_kwargs) - article.add_revision(ArticleRevision(title=title, **revision_kwargs), - save=True) + article.add_revision(ArticleRevision(title=title, **revision_kwargs), save=True) article.save() newpath = cls.objects.create( - site=site, - parent=parent, - slug=slug, - article=article) + site=site, parent=parent, slug=slug, article=article + ) article.add_object_relation(newpath) return newpath @classmethod def _create_urlpath_from_request( - cls, - request, - perm_article, - parent_urlpath, - slug, - title, - content, - summary): + cls, request, perm_article, parent_urlpath, slug, title, content, summary + ): """ Creates a new URLPath, using meta data from ``request`` and copies in the permissions from ``perm_article``. @@ -315,9 +298,9 @@ class URLPath(MPTTModel): if not request.user.is_anonymous: user = request.user if settings.LOG_IPS_USERS: - ip_address = request.META.get('REMOTE_ADDR', None) + ip_address = request.META.get("REMOTE_ADDR", None) elif settings.LOG_IPS_ANONYMOUS: - ip_address = request.META.get('REMOTE_ADDR', None) + ip_address = request.META.get("REMOTE_ADDR", None) return cls.create_urlpath( parent_urlpath, @@ -327,22 +310,27 @@ class URLPath(MPTTModel): user_message=summary, user=user, ip_address=ip_address, - article_kwargs={'owner': user, - 'group': perm_article.group, - 'group_read': perm_article.group_read, - 'group_write': perm_article.group_write, - 'other_read': perm_article.other_read, - 'other_write': perm_article.other_write} + article_kwargs={ + "owner": user, + "group": perm_article.group, + "group_read": perm_article.group_read, + "group_write": perm_article.group_write, + "other_read": perm_article.other_read, + "other_write": perm_article.other_write, + }, ) @classmethod def create_article(cls, *args, **kwargs): - warnings.warn("Pending removal: URLPath.create_article renamed to create_urlpath", DeprecationWarning) + warnings.warn( + "Pending removal: URLPath.create_article renamed to create_urlpath", + DeprecationWarning, + ) return cls.create_urlpath(*args, **kwargs) def get_ordered_children(self): """Return an ordered list of all chilren""" - return self.children.order_by('slug') + return self.children.order_by("slug") ###################################################### @@ -356,13 +344,11 @@ urlpath_content_type = None @disable_signal_for_loaddata def on_article_relation_save(**kwargs): global urlpath_content_type - instance = kwargs['instance'] + instance = kwargs["instance"] if not urlpath_content_type: urlpath_content_type = ContentType.objects.get_for_model(URLPath) if instance.content_type == urlpath_content_type: - URLPath.objects.filter( - id=instance.object_id).update( - article=instance.article) + URLPath.objects.filter(id=instance.object_id).update(article=instance.article) post_save.connect(on_article_relation_save, ArticleForObject) @@ -383,7 +369,7 @@ def on_article_delete(instance, *args, **kwargs): # Get the Lost-and-found path or create a new one # Only create the lost-and-found article if it's necessary and such # that the lost-and-found article can be deleted without being recreated! - ns = Namespace() # nonlocal namespace backported to Python 2.x + ns = Namespace() # nonlocal namespace backported to Python 2.x ns.lost_and_found = None def get_lost_and_found(): @@ -391,32 +377,32 @@ def on_article_delete(instance, *args, **kwargs): return ns.lost_and_found try: ns.lost_and_found = URLPath.objects.get( - slug=settings.LOST_AND_FOUND_SLUG, - parent=URLPath.root(), - site=site) + slug=settings.LOST_AND_FOUND_SLUG, parent=URLPath.root(), site=site + ) except URLPath.DoesNotExist: - article = Article(group_read=True, - group_write=False, - other_read=False, - other_write=False) + article = Article( + group_read=True, group_write=False, other_read=False, other_write=False + ) article.add_revision( ArticleRevision( content=_( - 'Articles who lost their parents\n' - '===============================\n\n' - 'The children of this article have had their parents deleted. You should probably find a new home for them.'), - title=_("Lost and found"))) + "Articles who lost their parents\n" + "===============================\n\n" + "The children of this article have had their parents deleted. You should probably find a new home for them." + ), + title=_("Lost and found"), + ) + ) ns.lost_and_found = URLPath.objects.create( slug=settings.LOST_AND_FOUND_SLUG, parent=URLPath.root(), site=site, - article=article) + article=article, + ) article.add_object_relation(ns.lost_and_found) return ns.lost_and_found - for urlpath in URLPath.objects.filter( - articles__article=instance, - site=site): + for urlpath in URLPath.objects.filter(articles__article=instance, site=site): # Delete the children for child in urlpath.get_children(): child.move_to(get_lost_and_found()) diff --git a/src/wiki/plugins/attachments/__init__.py b/src/wiki/plugins/attachments/__init__.py index c747810e..28cd24bb 100644 --- a/src/wiki/plugins/attachments/__init__.py +++ b/src/wiki/plugins/attachments/__init__.py @@ -1 +1 @@ -default_app_config = 'wiki.plugins.attachments.apps.AttachmentsConfig' +default_app_config = "wiki.plugins.attachments.apps.AttachmentsConfig" diff --git a/src/wiki/plugins/attachments/admin.py b/src/wiki/plugins/attachments/admin.py index 45c697c4..2f50590b 100644 --- a/src/wiki/plugins/attachments/admin.py +++ b/src/wiki/plugins/attachments/admin.py @@ -6,7 +6,7 @@ from . import models class AttachmentRevisionAdmin(admin.TabularInline): model = models.AttachmentRevision extra = 1 - fields = ('file', 'user', 'user_message') + fields = ("file", "user", "user_message") class AttachmentAdmin(admin.ModelAdmin): diff --git a/src/wiki/plugins/attachments/apps.py b/src/wiki/plugins/attachments/apps.py index 7a9a347e..4ae60b8e 100644 --- a/src/wiki/plugins/attachments/apps.py +++ b/src/wiki/plugins/attachments/apps.py @@ -3,6 +3,6 @@ from django.utils.translation import gettext_lazy as _ class AttachmentsConfig(AppConfig): - name = 'wiki.plugins.attachments' + name = "wiki.plugins.attachments" verbose_name = _("Wiki attachments") - label = 'wiki_attachments' + label = "wiki_attachments" diff --git a/src/wiki/plugins/attachments/forms.py b/src/wiki/plugins/attachments/forms.py index 8de0046b..7b124a5a 100644 --- a/src/wiki/plugins/attachments/forms.py +++ b/src/wiki/plugins/attachments/forms.py @@ -12,19 +12,19 @@ from wiki.plugins.attachments.models import IllegalFileExtension class AttachmentForm(forms.ModelForm): description = forms.CharField( - label=_('Description'), - help_text=_('A short summary of what the file contains'), - required=False + label=_("Description"), + help_text=_("A short summary of what the file contains"), + required=False, ) def __init__(self, *args, **kwargs): - self.article = kwargs.pop('article', None) - self.request = kwargs.pop('request', None) - self.attachment = kwargs.pop('attachment', None) + self.article = kwargs.pop("article", None) + self.request = kwargs.pop("request", None) + self.attachment = kwargs.pop("attachment", None) super().__init__(*args, **kwargs) def clean_file(self): - uploaded_file = self.cleaned_data.get('file', None) + uploaded_file = self.cleaned_data.get("file", None) if uploaded_file: try: models.extension_allowed(uploaded_file.name) @@ -33,12 +33,12 @@ class AttachmentForm(forms.ModelForm): return uploaded_file def save(self, *args, **kwargs): - commit = kwargs.get('commit', True) + commit = kwargs.get("commit", True) attachment_revision = super().save(commit=False) # Added because of AttachmentArchiveForm removing file from fields # should be more elegant - attachment_revision.file = self.cleaned_data['file'] + attachment_revision.file = self.cleaned_data["file"] if not self.attachment: attachment = models.Attachment() attachment.article = self.article @@ -56,15 +56,19 @@ class AttachmentForm(forms.ModelForm): class Meta: model = models.AttachmentRevision - fields = ('file', 'description',) + fields = ( + "file", + "description", + ) class AttachmentReplaceForm(AttachmentForm): replace = forms.BooleanField( - label=_('Remove previous'), - help_text=_('Remove previous attachment revisions and their files (to ' - 'save space)?'), + label=_("Remove previous"), + help_text=_( + "Remove previous attachment revisions and their files (to " "save space)?" + ), required=False, ) @@ -72,26 +76,27 @@ class AttachmentReplaceForm(AttachmentForm): class AttachmentArchiveForm(AttachmentForm): file = forms.FileField( # @ReservedAssignment - label=_('File or zip archive'), - required=True + label=_("File or zip archive"), required=True ) unzip_archive = forms.BooleanField( - label=_('Unzip file'), + label=_("Unzip file"), help_text=_( - 'Create individual attachments for files in a .zip file - directories do not work.'), - required=False) + "Create individual attachments for files in a .zip file - directories do not work." + ), + required=False, + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - ordered_fields = ['unzip_archive', 'file'] - self.fields.keyOrder = ordered_fields + [k - for k in self.fields.keys() - if k not in ordered_fields] + ordered_fields = ["unzip_archive", "file"] + self.fields.keyOrder = ordered_fields + [ + k for k in self.fields.keys() if k not in ordered_fields + ] def clean_file(self): - uploaded_file = self.cleaned_data.get('file', None) - if uploaded_file and self.cleaned_data.get('unzip_archive', False): + uploaded_file = self.cleaned_data.get("file", None) + if uploaded_file and self.cleaned_data.get("unzip_archive", False): try: self.zipfile = zipfile.ZipFile(uploaded_file.file, mode="r") for zipinfo in self.zipfile.filelist: @@ -109,20 +114,21 @@ class AttachmentArchiveForm(AttachmentForm): super().clean() if not can_moderate(self.article, self.request.user): raise forms.ValidationError( - gettext("User not allowed to moderate this article")) + gettext("User not allowed to moderate this article") + ) return self.cleaned_data def save(self, *args, **kwargs): # This is not having the intended effect - if 'file' not in self._meta.fields: - self._meta.fields.append('file') + if "file" not in self._meta.fields: + self._meta.fields.append("file") - if self.cleaned_data['unzip_archive']: + if self.cleaned_data["unzip_archive"]: new_attachments = [] try: for zipinfo in self.zipfile.filelist: - f = tempfile.NamedTemporaryFile(mode='r+w') + f = tempfile.NamedTemporaryFile(mode="r+w") f.write(self.zipfile.read(zipinfo.filename)) f = File(f, name=zipinfo.filename) try: @@ -133,7 +139,9 @@ class AttachmentArchiveForm(AttachmentForm): attachment.articles.add(self.article) attachment_revision = models.AttachmentRevision() attachment_revision.file = f - attachment_revision.description = self.cleaned_data['description'] + attachment_revision.description = self.cleaned_data[ + "description" + ] attachment_revision.attachment = attachment attachment_revision.set_from_request(self.request) attachment_revision.save() @@ -148,22 +156,24 @@ class AttachmentArchiveForm(AttachmentForm): return super().save(*args, **kwargs) class Meta(AttachmentForm.Meta): - fields = ['description', ] + fields = [ + "description", + ] class DeleteForm(forms.Form): """This form is both used for dereferencing and deleting attachments""" - confirm = forms.BooleanField(label=_('Yes I am sure...'), required=False) + + confirm = forms.BooleanField(label=_("Yes I am sure..."), required=False) def clean_confirm(self): - if not self.cleaned_data['confirm']: - raise forms.ValidationError(gettext('You are not sure enough!')) + if not self.cleaned_data["confirm"]: + raise forms.ValidationError(gettext("You are not sure enough!")) return True class SearchForm(forms.Form): query = forms.CharField( - label="", - widget=forms.TextInput(attrs={'class': 'search-query form-control'}), + label="", widget=forms.TextInput(attrs={"class": "search-query form-control"}), ) diff --git a/src/wiki/plugins/attachments/markdown_extensions.py b/src/wiki/plugins/attachments/markdown_extensions.py index 79ce8561..f8378f45 100644 --- a/src/wiki/plugins/attachments/markdown_extensions.py +++ b/src/wiki/plugins/attachments/markdown_extensions.py @@ -8,8 +8,9 @@ from wiki.core.permissions import can_read from wiki.plugins.attachments import models ATTACHMENT_RE = re.compile( - r'(?P.*)\[( *((attachment\:(?P[0-9]+))|(title\:\"(?P[^\"]+)\")|(?P<size>size)))+\](?P<after>.*)', - re.IGNORECASE) + r"(?P<before>.*)\[( *((attachment\:(?P<id>[0-9]+))|(title\:\"(?P<title>[^\"]+)\")|(?P<size>size)))+\](?P<after>.*)", + re.IGNORECASE, +) class AttachmentExtension(markdown.Extension): @@ -19,9 +20,8 @@ class AttachmentExtension(markdown.Extension): def extendMarkdown(self, md): """ Insert AbbrPreprocessor before ReferencePreprocessor. """ md.preprocessors.add( - 'dw-attachments', - AttachmentPreprocessor(md), - '>html_block') + "dw-attachments", AttachmentPreprocessor(md), ">html_block" + ) class AttachmentPreprocessor(markdown.preprocessors.Preprocessor): @@ -36,23 +36,24 @@ class AttachmentPreprocessor(markdown.preprocessors.Preprocessor): new_text.append(line) continue - attachment_id = m.group('id').strip() - title = m.group('title') - size = m.group('size') - before = self.run([m.group('before')])[0] - after = self.run([m.group('after')])[0] + attachment_id = m.group("id").strip() + title = m.group("title") + size = m.group("size") + before = self.run([m.group("before")])[0] + after = self.run([m.group("after")])[0] try: attachment = models.Attachment.objects.get( articles__current_revision__deleted=False, - id=attachment_id, current_revision__deleted=False, - articles=self.markdown.article + id=attachment_id, + current_revision__deleted=False, + articles=self.markdown.article, ) url = reverse( - 'wiki:attachments_download', + "wiki:attachments_download", kwargs={ - 'article_id': self.markdown.article.id, - 'attachment_id': attachment.id, - } + "article_id": self.markdown.article.id, + "attachment_id": attachment.id, + }, ) # The readability of the attachment is decided relative @@ -68,17 +69,16 @@ class AttachmentPreprocessor(markdown.preprocessors.Preprocessor): if size: size = attachment.current_revision.get_size() - attachment_can_read = can_read( - self.markdown.article, article_owner) + attachment_can_read = can_read(self.markdown.article, article_owner) html = render_to_string( "wiki/plugins/attachments/render.html", context={ - 'url': url, - 'filename': attachment.original_filename, - 'title': title, - 'size': size, - 'attachment_can_read': attachment_can_read, - } + "url": url, + "filename": attachment.original_filename, + "title": title, + "size": size, + "attachment_can_read": attachment_can_read, + }, ) line = self.markdown.htmlStash.store(html) except models.Attachment.DoesNotExist: @@ -87,8 +87,7 @@ class AttachmentPreprocessor(markdown.preprocessors.Preprocessor): """#{} is deleted.</span>""" ).format(attachment_id) line = line.replace( - '[' + m.group(2) + ']', - self.markdown.htmlStash.store(html) + "[" + m.group(2) + "]", self.markdown.htmlStash.store(html) ) new_text.append(before + line + after) return new_text diff --git a/src/wiki/plugins/attachments/migrations/0001_initial.py b/src/wiki/plugins/attachments/migrations/0001_initial.py index 11de0079..d0b0c437 100644 --- a/src/wiki/plugins/attachments/migrations/0001_initial.py +++ b/src/wiki/plugins/attachments/migrations/0001_initial.py @@ -8,53 +8,124 @@ from django.db.models.fields import GenericIPAddressField as IPAddressField class Migration(migrations.Migration): dependencies = [ - ('wiki', '0001_initial'), + ("wiki", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Attachment', + name="Attachment", fields=[ - ('reusableplugin_ptr', models.OneToOneField(parent_link=True, serialize=False, primary_key=True, to='wiki.ReusablePlugin', auto_created=True, on_delete=models.CASCADE)), - ('original_filename', models.CharField(max_length=256, verbose_name='original filename', blank=True, null=True)), + ( + "reusableplugin_ptr", + models.OneToOneField( + parent_link=True, + serialize=False, + primary_key=True, + to="wiki.ReusablePlugin", + auto_created=True, + on_delete=models.CASCADE, + ), + ), + ( + "original_filename", + models.CharField( + max_length=256, + verbose_name="original filename", + blank=True, + null=True, + ), + ), ], options={ - 'verbose_name': 'attachment', - 'verbose_name_plural': 'attachments', + "verbose_name": "attachment", + "verbose_name_plural": "attachments", }, - bases=('wiki.reusableplugin',), + bases=("wiki.reusableplugin",), ), migrations.CreateModel( - name='AttachmentRevision', + name="AttachmentRevision", fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)), - ('revision_number', models.IntegerField(verbose_name='revision number', editable=False)), - ('user_message', models.TextField(blank=True)), - ('automatic_log', models.TextField(editable=False, blank=True)), - ('ip_address', IPAddressField(editable=False, verbose_name='IP address', blank=True, null=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('created', models.DateTimeField(auto_now_add=True)), - ('deleted', models.BooleanField(default=False, verbose_name='deleted')), - ('locked', models.BooleanField(default=False, verbose_name='locked')), - ('file', models.FileField(max_length=255, verbose_name='file', upload_to=wiki.plugins.attachments.models.upload_path)), - ('description', models.TextField(blank=True)), - ('attachment', models.ForeignKey(to='wiki_attachments.Attachment', on_delete=models.CASCADE)), - ('previous_revision', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.SET_NULL, to='wiki_attachments.AttachmentRevision', null=True)), - ('user', models.ForeignKey(blank=True, verbose_name='user', on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, null=True)), + ( + "id", + models.AutoField( + serialize=False, + primary_key=True, + verbose_name="ID", + auto_created=True, + ), + ), + ( + "revision_number", + models.IntegerField(verbose_name="revision number", editable=False), + ), + ("user_message", models.TextField(blank=True)), + ("automatic_log", models.TextField(editable=False, blank=True)), + ( + "ip_address", + IPAddressField( + editable=False, verbose_name="IP address", blank=True, null=True + ), + ), + ("modified", models.DateTimeField(auto_now=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("deleted", models.BooleanField(default=False, verbose_name="deleted")), + ("locked", models.BooleanField(default=False, verbose_name="locked")), + ( + "file", + models.FileField( + max_length=255, + verbose_name="file", + upload_to=wiki.plugins.attachments.models.upload_path, + ), + ), + ("description", models.TextField(blank=True)), + ( + "attachment", + models.ForeignKey( + to="wiki_attachments.Attachment", on_delete=models.CASCADE + ), + ), + ( + "previous_revision", + models.ForeignKey( + blank=True, + on_delete=django.db.models.deletion.SET_NULL, + to="wiki_attachments.AttachmentRevision", + null=True, + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + verbose_name="user", + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + null=True, + ), + ), ], options={ - 'ordering': ('created',), - 'get_latest_by': 'revision_number', - 'verbose_name': 'attachment revision', - 'verbose_name_plural': 'attachment revisions', + "ordering": ("created",), + "get_latest_by": "revision_number", + "verbose_name": "attachment revision", + "verbose_name_plural": "attachment revisions", }, bases=(models.Model,), ), migrations.AddField( - model_name='attachment', - name='current_revision', - field=models.OneToOneField(to='wiki_attachments.AttachmentRevision', blank=True, verbose_name='current revision', related_name='current_set', help_text='The revision of this attachment currently in use (on all articles using the attachment)', null=True, on_delete=models.CASCADE), + model_name="attachment", + name="current_revision", + field=models.OneToOneField( + to="wiki_attachments.AttachmentRevision", + blank=True, + verbose_name="current revision", + related_name="current_set", + help_text="The revision of this attachment currently in use (on all articles using the attachment)", + null=True, + on_delete=models.CASCADE, + ), preserve_default=True, ), ] diff --git a/src/wiki/plugins/attachments/migrations/0002_auto_20151118_1816.py b/src/wiki/plugins/attachments/migrations/0002_auto_20151118_1816.py index b43cb171..30aa35f6 100644 --- a/src/wiki/plugins/attachments/migrations/0002_auto_20151118_1816.py +++ b/src/wiki/plugins/attachments/migrations/0002_auto_20151118_1816.py @@ -6,16 +6,14 @@ class Migration(migrations.Migration): atomic = False dependencies = [ - ('wiki_attachments', '0001_initial'), + ("wiki_attachments", "0001_initial"), ] operations = [ migrations.AlterModelTable( - name='attachment', - table='wiki_attachments_attachment', + name="attachment", table="wiki_attachments_attachment", ), migrations.AlterModelTable( - name='attachmentrevision', - table='wiki_attachments_attachmentrevision', + name="attachmentrevision", table="wiki_attachments_attachmentrevision", ), ] diff --git a/src/wiki/plugins/attachments/models.py b/src/wiki/plugins/attachments/models.py index 84c1e833..36ed0c69 100644 --- a/src/wiki/plugins/attachments/models.py +++ b/src/wiki/plugins/attachments/models.py @@ -15,6 +15,7 @@ from . import settings class IllegalFileExtension(Exception): """File extension on upload is not allowed""" + pass @@ -23,17 +24,20 @@ class Attachment(ReusablePlugin): objects = managers.ArticleFkManager() current_revision = models.OneToOneField( - 'AttachmentRevision', verbose_name=_('current revision'), - blank=True, null=True, related_name='current_set', + "AttachmentRevision", + verbose_name=_("current revision"), + blank=True, + null=True, + related_name="current_set", on_delete=models.CASCADE, help_text=_( - 'The revision of this attachment currently in use (on all articles using the attachment)'),) + "The revision of this attachment currently in use (on all articles using the attachment)" + ), + ) original_filename = models.CharField( - max_length=256, - verbose_name=_('original filename'), - blank=True, - null=True) + max_length=256, verbose_name=_("original filename"), blank=True, null=True + ) def can_write(self, user): if not settings.ANONYMOUS and (not user or user.is_anonymous): @@ -44,16 +48,19 @@ class Attachment(ReusablePlugin): return self.can_write(user) class Meta: - verbose_name = _('attachment') - verbose_name_plural = _('attachments') + verbose_name = _("attachment") + verbose_name_plural = _("attachments") # Matches label of upcoming 0.1 release - db_table = 'wiki_attachments_attachment' + db_table = "wiki_attachments_attachment" def __str__(self): from wiki.models import Article + try: return "%s: %s" % ( - self.article.current_revision.title, self.original_filename) + self.article.current_revision.title, + self.original_filename, + ) except Article.DoesNotExist: return "Attachment for non-existing article" @@ -64,18 +71,14 @@ def extension_allowed(filename): except IndexError: # No extension raise IllegalFileExtension( - gettext("No file extension found in filename. That's not okay!")) - if not extension.lower() in map( - lambda x: x.lower(), - settings.FILE_EXTENSIONS): + gettext("No file extension found in filename. That's not okay!") + ) + if not extension.lower() in map(lambda x: x.lower(), settings.FILE_EXTENSIONS): raise IllegalFileExtension( gettext( "The following filename is illegal: {filename:s}. Extension " "has to be one of {extensions:s}" - ).format( - filename=filename, - extensions=", ".join(settings.FILE_EXTENSIONS) - ) + ).format(filename=filename, extensions=", ".join(settings.FILE_EXTENSIONS)) ) return extension @@ -89,45 +92,46 @@ def upload_path(instance, filename): original_extension = instance.attachment.original_filename.split(".")[-1] if not extension.lower() == original_extension: raise IllegalFileExtension( - "File extension has to be '%s', not '%s'." % - (original_extension, extension.lower())) + "File extension has to be '%s', not '%s'." + % (original_extension, extension.lower()) + ) elif instance.attachment: instance.attachment.original_filename = filename upload_path = settings.UPLOAD_PATH - upload_path = upload_path.replace( - '%aid', str( - instance.attachment.article.id)) + upload_path = upload_path.replace("%aid", str(instance.attachment.article.id)) if settings.UPLOAD_PATH_OBSCURIFY: import random import hashlib - m = hashlib.md5( - str(random.randint(0, 100000000000000)).encode('ascii')) + + m = hashlib.md5(str(random.randint(0, 100000000000000)).encode("ascii")) upload_path = os.path.join(upload_path, m.hexdigest()) if settings.APPEND_EXTENSION: - filename += '.upload' + filename += ".upload" return os.path.join(upload_path, filename) class AttachmentRevision(BaseRevisionMixin, models.Model): - attachment = models.ForeignKey('Attachment', on_delete=models.CASCADE) + attachment = models.ForeignKey("Attachment", on_delete=models.CASCADE) - file = models.FileField(upload_to=upload_path, # @ReservedAssignment - max_length=255, - verbose_name=_('file'), - storage=settings.STORAGE_BACKEND) + file = models.FileField( + upload_to=upload_path, # @ReservedAssignment + max_length=255, + verbose_name=_("file"), + storage=settings.STORAGE_BACKEND, + ) description = models.TextField(blank=True) class Meta: - verbose_name = _('attachment revision') - verbose_name_plural = _('attachment revisions') - ordering = ('created',) - get_latest_by = 'revision_number' + verbose_name = _("attachment revision") + verbose_name_plural = _("attachment revisions") + ordering = ("created",) + get_latest_by = "revision_number" # Matches label of upcoming 0.1 release - db_table = 'wiki_attachments_attachmentrevision' + db_table = "wiki_attachments_attachmentrevision" def get_filename(self): """Used to retrieve the filename of a revision. @@ -147,9 +151,11 @@ class AttachmentRevision(BaseRevisionMixin, models.Model): return None def __str__(self): - return "%s: %s (r%d)" % (self.attachment.article.current_revision.title, - self.attachment.original_filename, - self.revision_number) + return "%s: %s (r%d)" % ( + self.attachment.article.current_revision.title, + self.attachment.original_filename, + self.revision_number, + ) @disable_signal_for_loaddata @@ -172,7 +178,10 @@ def on_revision_delete(instance, *args, **kwargs): for depth in range(0, max_depth): delete_path = "/".join(path[:-depth] if depth > 0 else path) try: - if len(os.listdir(os.path.join(django_settings.MEDIA_ROOT, delete_path))) == 0: + if ( + len(os.listdir(os.path.join(django_settings.MEDIA_ROOT, delete_path))) + == 0 + ): os.rmdir(delete_path) except OSError: # Raised by os.listdir if directory is missing @@ -181,13 +190,13 @@ def on_revision_delete(instance, *args, **kwargs): @disable_signal_for_loaddata def on_attachment_revision_pre_save(**kwargs): - instance = kwargs['instance'] + instance = kwargs["instance"] if instance._state.adding: update_previous_revision = ( - not instance.previous_revision and - instance.attachment and - instance.attachment.current_revision and - instance.attachment.current_revision != instance + not instance.previous_revision + and instance.attachment + and instance.attachment.current_revision + and instance.attachment.current_revision != instance ) if update_previous_revision: instance.previous_revision = instance.attachment.current_revision @@ -204,7 +213,7 @@ def on_attachment_revision_pre_save(**kwargs): @disable_signal_for_loaddata def on_attachment_revision_post_save(**kwargs): - instance = kwargs['instance'] + instance = kwargs["instance"] if not instance.attachment.current_revision: # If I'm saved from Django admin, then article.current_revision is # me! diff --git a/src/wiki/plugins/attachments/settings.py b/src/wiki/plugins/attachments/settings.py index 167e716d..136e11e2 100644 --- a/src/wiki/plugins/attachments/settings.py +++ b/src/wiki/plugins/attachments/settings.py @@ -14,9 +14,8 @@ SLUG = "attachments" #: ``WIKI_ATTACHMENTS_ANONYMOUS`` can override this, otherwise the default #: in ``wiki.conf.settings`` is used. ANONYMOUS = getattr( - django_settings, - 'WIKI_ATTACHMENTS_ANONYMOUS', - wiki_settings.ANONYMOUS_UPLOAD) + django_settings, "WIKI_ATTACHMENTS_ANONYMOUS", wiki_settings.ANONYMOUS_UPLOAD +) # Maximum file sizes: Please use something like LimitRequestBody on # your web server. @@ -27,18 +26,16 @@ ANONYMOUS = getattr( #: Actually, you can completely disable serving it, if you want. Files are #: sent to the user through a Django view that reads and streams a file. UPLOAD_PATH = getattr( - django_settings, - 'WIKI_ATTACHMENTS_PATH', - 'wiki/attachments/%aid/') + django_settings, "WIKI_ATTACHMENTS_PATH", "wiki/attachments/%aid/" +) #: Should the upload path be obscurified? If so, a random hash will be #: added to the path such that someone can not guess the location of files #: (if you have restricted permissions and the files are still located #: within the web server's file system). UPLOAD_PATH_OBSCURIFY = getattr( - django_settings, - 'WIKI_ATTACHMENTS_PATH_OBSCURIFY', - True) + django_settings, "WIKI_ATTACHMENTS_PATH_OBSCURIFY", True +) #: Allowed extensions for attachments, empty to disallow uploads completely. #: If ``WIKI_ATTACHMENTS_APPEND_EXTENSION`` files are saved with an appended @@ -48,29 +45,25 @@ UPLOAD_PATH_OBSCURIFY = getattr( #: to allow. For your own safety. #: Note: this setting is called WIKI_ATTACHMENTS_EXTENTIONS not WIKI_ATTACHMENTS_FILE_EXTENTIONS FILE_EXTENSIONS = getattr( - django_settings, 'WIKI_ATTACHMENTS_EXTENSIONS', - ['pdf', 'doc', 'odt', 'docx', 'txt']) + django_settings, "WIKI_ATTACHMENTS_EXTENSIONS", ["pdf", "doc", "odt", "docx", "txt"] +) #: Storage backend to use, default is to use the same as the rest of the #: wiki, which is set in ``WIKI_STORAGE_BACKEND``, but you can override it #: with ``WIKI_ATTACHMENTS_STORAGE_BACKEND``. STORAGE_BACKEND = getattr( - django_settings, - 'WIKI_ATTACHMENTS_STORAGE_BACKEND', - wiki_settings.STORAGE_BACKEND) + django_settings, "WIKI_ATTACHMENTS_STORAGE_BACKEND", wiki_settings.STORAGE_BACKEND +) #: Store files always with an appended .upload extension to be sure that #: something nasty does not get executed on the server. SAFETY FIRST! -APPEND_EXTENSION = getattr( - django_settings, - 'WIKI_ATTACHMENTS_APPEND_EXTENSION', - True) +APPEND_EXTENSION = getattr(django_settings, "WIKI_ATTACHMENTS_APPEND_EXTENSION", True) #: Important for e.g. S3 backends: If your storage backend does not have a .path #: attribute for the file, but only a .url attribute, you should use False. #: This will reveal the direct download URL so it does not work perfectly for #: files you wish to be kept private. -USE_LOCAL_PATH = getattr(django_settings, 'WIKI_ATTACHMENTS_LOCAL_PATH', True) +USE_LOCAL_PATH = getattr(django_settings, "WIKI_ATTACHMENTS_LOCAL_PATH", True) if (not USE_LOCAL_PATH) and APPEND_EXTENSION: raise ImproperlyConfigured( @@ -78,4 +71,5 @@ if (not USE_LOCAL_PATH) and APPEND_EXTENSION: "You have configured to append .upload and not use local paths. That won't " "work as all your attachments will be stored and sent with a .upload " "extension. You have to trust your storage backend to be safe for storing" - "the extensions you have allowed.") + "the extensions you have allowed." + ) diff --git a/src/wiki/plugins/attachments/urls.py b/src/wiki/plugins/attachments/urls.py index 54323175..f91a605b 100644 --- a/src/wiki/plugins/attachments/urls.py +++ b/src/wiki/plugins/attachments/urls.py @@ -2,31 +2,43 @@ from django.urls import re_path from wiki.plugins.attachments import views urlpatterns = [ - re_path(r'^$', - views.AttachmentView.as_view(), - name='attachments_index'), - re_path(r'^search/$', - views.AttachmentSearchView.as_view(), - name='attachments_search'), - re_path(r'^add/(?P<attachment_id>[0-9]+)/$', + re_path(r"^$", views.AttachmentView.as_view(), name="attachments_index"), + re_path( + r"^search/$", views.AttachmentSearchView.as_view(), name="attachments_search" + ), + re_path( + r"^add/(?P<attachment_id>[0-9]+)/$", views.AttachmentAddView.as_view(), - name='attachments_add'), - re_path(r'^replace/(?P<attachment_id>[0-9]+)/$', + name="attachments_add", + ), + re_path( + r"^replace/(?P<attachment_id>[0-9]+)/$", views.AttachmentReplaceView.as_view(), - name='attachments_replace'), - re_path(r'^history/(?P<attachment_id>[0-9]+)/$', + name="attachments_replace", + ), + re_path( + r"^history/(?P<attachment_id>[0-9]+)/$", views.AttachmentHistoryView.as_view(), - name='attachments_history'), - re_path(r'^download/(?P<attachment_id>[0-9]+)/$', + name="attachments_history", + ), + re_path( + r"^download/(?P<attachment_id>[0-9]+)/$", views.AttachmentDownloadView.as_view(), - name='attachments_download'), - re_path(r'^delete/(?P<attachment_id>[0-9]+)/$', + name="attachments_download", + ), + re_path( + r"^delete/(?P<attachment_id>[0-9]+)/$", views.AttachmentDeleteView.as_view(), - name='attachments_delete'), - re_path(r'^download/(?P<attachment_id>[0-9]+)/revision/(?P<revision_id>[0-9]+)/$', + name="attachments_delete", + ), + re_path( + r"^download/(?P<attachment_id>[0-9]+)/revision/(?P<revision_id>[0-9]+)/$", views.AttachmentDownloadView.as_view(), - name='attachments_download'), - re_path(r'^change/(?P<attachment_id>[0-9]+)/revision/(?P<revision_id>[0-9]+)/$', + name="attachments_download", + ), + re_path( + r"^change/(?P<attachment_id>[0-9]+)/revision/(?P<revision_id>[0-9]+)/$", views.AttachmentChangeRevisionView.as_view(), - name='attachments_revision_change'), + name="attachments_revision_change", + ), ] diff --git a/src/wiki/plugins/attachments/views.py b/src/wiki/plugins/attachments/views.py index a4ae0da2..4fa630a3 100644 --- a/src/wiki/plugins/attachments/views.py +++ b/src/wiki/plugins/attachments/views.py @@ -21,16 +21,19 @@ class AttachmentView(ArticleMixin, FormView): @method_decorator(get_article(can_read=True)) def dispatch(self, request, article, *args, **kwargs): if article.can_moderate(request.user): - self.attachments = models.Attachment.objects.filter( - articles=article, current_revision__deleted=False - ).exclude( - current_revision__file=None - ).order_by('original_filename') + self.attachments = ( + models.Attachment.objects.filter( + articles=article, current_revision__deleted=False + ) + .exclude(current_revision__file=None) + .order_by("original_filename") + ) self.form_class = forms.AttachmentArchiveForm else: self.attachments = models.Attachment.objects.active().filter( - articles=article) + articles=article + ) # Fixing some weird transaction issue caused by adding commit_manually # to form_valid @@ -38,47 +41,52 @@ class AttachmentView(ArticleMixin, FormView): def form_valid(self, form): - if (self.request.user.is_anonymous and not settings.ANONYMOUS or - not self.article.can_write(self.request.user) or - self.article.current_revision.locked): + if ( + self.request.user.is_anonymous + and not settings.ANONYMOUS + or not self.article.can_write(self.request.user) + or self.article.current_revision.locked + ): return response_forbidden(self.request, self.article, self.urlpath) attachment_revision = form.save() if isinstance(attachment_revision, list): messages.success( - self.request, _('Successfully added: %s') % - (", ".join( - [ar.get_filename() for ar in attachment_revision]))) + self.request, + _("Successfully added: %s") + % (", ".join([ar.get_filename() for ar in attachment_revision])), + ) else: messages.success( self.request, - _('%s was successfully added.') % - attachment_revision.get_filename()) + _("%s was successfully added.") % attachment_revision.get_filename(), + ) self.article.clear_cache() return redirect( - "wiki:attachments_index", - path=self.urlpath.path, - article_id=self.article.id) + "wiki:attachments_index", path=self.urlpath.path, article_id=self.article.id + ) def get_form_kwargs(self): kwargs = super().get_form_kwargs() - kwargs['article'] = self.article - kwargs['request'] = self.request + kwargs["article"] = self.article + kwargs["request"] = self.request return kwargs def get_context_data(self, **kwargs): # Needed since Django 1.9 because get_context_data is no longer called # with the form instance - if 'form' not in kwargs: - kwargs['form'] = self.get_form() - kwargs['attachments'] = self.attachments - kwargs['deleted_attachments'] = models.Attachment.objects.filter( - articles=self.article, - current_revision__deleted=True) - kwargs['search_form'] = forms.SearchForm() - kwargs['selected_tab'] = 'attachments' - kwargs['anonymous_disallowed'] = self.request.user.is_anonymous and not settings.ANONYMOUS + if "form" not in kwargs: + kwargs["form"] = self.get_form() + kwargs["attachments"] = self.attachments + kwargs["deleted_attachments"] = models.Attachment.objects.filter( + articles=self.article, current_revision__deleted=True + ) + kwargs["search_form"] = forms.SearchForm() + kwargs["selected_tab"] = "attachments" + kwargs["anonymous_disallowed"] = ( + self.request.user.is_anonymous and not settings.ANONYMOUS + ) return super().get_context_data(**kwargs) @@ -90,21 +98,20 @@ class AttachmentHistoryView(ArticleMixin, TemplateView): def dispatch(self, request, article, attachment_id, *args, **kwargs): if article.can_moderate(request.user): self.attachment = get_object_or_404( - models.Attachment, - id=attachment_id, - articles=article) + models.Attachment, id=attachment_id, articles=article + ) else: self.attachment = get_object_or_404( - models.Attachment.objects.active(), - id=attachment_id, - articles=article) + models.Attachment.objects.active(), id=attachment_id, articles=article + ) return super().dispatch(request, article, *args, **kwargs) def get_context_data(self, **kwargs): - kwargs['attachment'] = self.attachment - kwargs['revisions'] = self.attachment.attachmentrevision_set.all().order_by( - '-revision_number') - kwargs['selected_tab'] = 'attachments' + kwargs["attachment"] = self.attachment + kwargs["revisions"] = self.attachment.attachmentrevision_set.all().order_by( + "-revision_number" + ) + kwargs["selected_tab"] = "attachments" return super().get_context_data(**kwargs) @@ -116,18 +123,16 @@ class AttachmentReplaceView(ArticleMixin, FormView): @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, attachment_id, *args, **kwargs): if request.user.is_anonymous and not settings.ANONYMOUS: - return response_forbidden(request, article, kwargs.get('urlpath', None)) + return response_forbidden(request, article, kwargs.get("urlpath", None)) if article.can_moderate(request.user): self.attachment = get_object_or_404( - models.Attachment, - id=attachment_id, - articles=article) + models.Attachment, id=attachment_id, articles=article + ) self.can_moderate = True else: self.attachment = get_object_or_404( - models.Attachment.objects.active(), - id=attachment_id, - articles=article) + models.Attachment.objects.active(), id=attachment_id, articles=article + ) self.can_moderate = False return super().dispatch(request, article, *args, **kwargs) @@ -148,24 +153,27 @@ class AttachmentReplaceView(ArticleMixin, FormView): self.attachment.save() messages.success( self.request, - _('%s uploaded and replaces old attachment.') % - attachment_revision.get_filename()) + _("%s uploaded and replaces old attachment.") + % attachment_revision.get_filename(), + ) self.article.clear_cache() except models.IllegalFileExtension as e: - messages.error(self.request, _('Your file could not be saved: %s') % e) + messages.error(self.request, _("Your file could not be saved: %s") % e) return redirect( "wiki:attachments_replace", attachment_id=self.attachment.id, path=self.urlpath.path, - article_id=self.article.id) + article_id=self.article.id, + ) if self.can_moderate: - if form.cleaned_data['replace']: + if form.cleaned_data["replace"]: # form has no cleaned_data field unless self.can_moderate is True try: most_recent_revision = self.attachment.attachmentrevision_set.exclude( id=attachment_revision.id, - created__lte=attachment_revision.created).latest() + created__lte=attachment_revision.created, + ).latest() most_recent_revision.delete() except ObjectDoesNotExist: msg = "{attachment} does not contain any revisions.".format( @@ -174,54 +182,50 @@ class AttachmentReplaceView(ArticleMixin, FormView): messages.error(self.request, msg) return redirect( - "wiki:attachments_index", - path=self.urlpath.path, - article_id=self.article.id) + "wiki:attachments_index", path=self.urlpath.path, article_id=self.article.id + ) def get_form(self, form_class=None): form = super().get_form(form_class=form_class) - form.fields['file'].help_text = _( - 'Your new file will automatically be renamed to match the file already present. Files with different extensions are not allowed.') + form.fields["file"].help_text = _( + "Your new file will automatically be renamed to match the file already present. Files with different extensions are not allowed." + ) return form def get_form_kwargs(self): kwargs = super().get_form_kwargs() - kwargs['article'] = self.article - kwargs['request'] = self.request - kwargs['attachment'] = self.attachment + kwargs["article"] = self.article + kwargs["request"] = self.request + kwargs["attachment"] = self.attachment return kwargs def get_initial(self, **kwargs): - return {'description': self.attachment.current_revision.description} + return {"description": self.attachment.current_revision.description} def get_context_data(self, **kwargs): - if 'form' not in kwargs: - kwargs['form'] = self.get_form() - kwargs['attachment'] = self.attachment - kwargs['selected_tab'] = 'attachments' + if "form" not in kwargs: + kwargs["form"] = self.get_form() + kwargs["attachment"] = self.attachment + kwargs["selected_tab"] = "attachments" return super().get_context_data(**kwargs) class AttachmentDownloadView(ArticleMixin, View): - @method_decorator(get_article(can_read=True)) def dispatch(self, request, article, attachment_id, *args, **kwargs): if article.can_moderate(request.user): self.attachment = get_object_or_404( - models.Attachment, - id=attachment_id, - articles=article) + models.Attachment, id=attachment_id, articles=article + ) else: self.attachment = get_object_or_404( - models.Attachment.objects.active(), - id=attachment_id, - articles=article) - revision_id = kwargs.get('revision_id', None) + models.Attachment.objects.active(), id=attachment_id, articles=article + ) + revision_id = kwargs.get("revision_id", None) if revision_id: self.revision = get_object_or_404( - models.AttachmentRevision, - id=revision_id, - attachment__articles=article) + models.AttachmentRevision, id=revision_id, attachment__articles=article + ) else: self.revision = self.attachment.current_revision return super().dispatch(request, article, *args, **kwargs) @@ -234,7 +238,8 @@ class AttachmentDownloadView(ArticleMixin, View): request, self.revision.file.path, self.revision.created, - self.attachment.original_filename) + self.attachment.original_filename, + ) except OSError: pass else: @@ -251,18 +256,15 @@ class AttachmentChangeRevisionView(ArticleMixin, View): def dispatch(self, request, article, attachment_id, revision_id, *args, **kwargs): if article.can_moderate(request.user): self.attachment = get_object_or_404( - models.Attachment, - id=attachment_id, - articles=article) + models.Attachment, id=attachment_id, articles=article + ) else: self.attachment = get_object_or_404( - models.Attachment.objects.active(), - id=attachment_id, - articles=article) + models.Attachment.objects.active(), id=attachment_id, articles=article + ) self.revision = get_object_or_404( - models.AttachmentRevision, - id=revision_id, - attachment__articles=article) + models.AttachmentRevision, id=revision_id, attachment__articles=article + ) return super().dispatch(request, article, *args, **kwargs) def post(self, request, *args, **kwargs): @@ -271,29 +273,26 @@ class AttachmentChangeRevisionView(ArticleMixin, View): self.article.clear_cache() messages.success( self.request, - _('Current revision changed for %s.') % - self.attachment.original_filename) + _("Current revision changed for %s.") % self.attachment.original_filename, + ) return redirect( - "wiki:attachments_index", - path=self.urlpath.path, - article_id=self.article.id) + "wiki:attachments_index", path=self.urlpath.path, article_id=self.article.id + ) def get_context_data(self, **kwargs): - kwargs['selected_tab'] = 'attachments' - if 'form' not in kwargs: - kwargs['form'] = self.get_form() + kwargs["selected_tab"] = "attachments" + if "form" not in kwargs: + kwargs["form"] = self.get_form() return ArticleMixin.get_context_data(self, **kwargs) class AttachmentAddView(ArticleMixin, View): - @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, attachment_id, *args, **kwargs): self.attachment = get_object_or_404( - models.Attachment.objects.active().can_write( - request.user), - id=attachment_id) + models.Attachment.objects.active().can_write(request.user), id=attachment_id + ) return super().dispatch(request, article, *args, **kwargs) def post(self, request, *args, **kwargs): @@ -303,17 +302,21 @@ class AttachmentAddView(ArticleMixin, View): self.article.clear_cache() messages.success( self.request, - _('Added a reference to "%(att)s" from "%(art)s".') % { - 'att': self.attachment.original_filename, - 'art': self.article.current_revision.title}) + _('Added a reference to "%(att)s" from "%(art)s".') + % { + "att": self.attachment.original_filename, + "art": self.article.current_revision.title, + }, + ) else: messages.error( - self.request, _('"%(att)s" is already referenced.') % - {'att': self.attachment.original_filename}) + self.request, + _('"%(att)s" is already referenced.') + % {"att": self.attachment.original_filename}, + ) return redirect( - "wiki:attachments_index", - path=self.urlpath.path, - article_id=self.article.id) + "wiki:attachments_index", path=self.urlpath.path, article_id=self.article.id + ) class AttachmentDeleteView(ArticleMixin, FormView): @@ -323,9 +326,11 @@ class AttachmentDeleteView(ArticleMixin, FormView): @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, attachment_id, *args, **kwargs): - self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article) + self.attachment = get_object_or_404( + models.Attachment, id=attachment_id, articles=article + ) if not self.attachment.can_delete(request.user): - return response_forbidden(request, article, kwargs.get('urlpath', None)) + return response_forbidden(request, article, kwargs.get("urlpath", None)) return super().dispatch(request, article, *args, **kwargs) def form_valid(self, form): @@ -335,27 +340,39 @@ class AttachmentDeleteView(ArticleMixin, FormView): revision.attachment = self.attachment revision.set_from_request(self.request) revision.deleted = True - revision.file = self.attachment.current_revision.file if self.attachment.current_revision else None - revision.description = self.attachment.current_revision.description if self.attachment.current_revision else "" + revision.file = ( + self.attachment.current_revision.file + if self.attachment.current_revision + else None + ) + revision.description = ( + self.attachment.current_revision.description + if self.attachment.current_revision + else "" + ) revision.save() self.attachment.current_revision = revision self.attachment.save() self.article.clear_cache() - messages.info(self.request, _('The file %s was deleted.') % self.attachment.original_filename) + messages.info( + self.request, + _("The file %s was deleted.") % self.attachment.original_filename, + ) else: self.attachment.articles.remove(self.article) messages.info( self.request, - _('This article is no longer related to the file %s.') % - self.attachment.original_filename) + _("This article is no longer related to the file %s.") + % self.attachment.original_filename, + ) self.article.clear_cache() return redirect("wiki:get", path=self.urlpath.path, article_id=self.article.id) def get_context_data(self, **kwargs): - kwargs['attachment'] = self.attachment - kwargs['selected_tab'] = 'attachments' - if 'form' not in kwargs: - kwargs['form'] = self.get_form() + kwargs["attachment"] = self.attachment + kwargs["selected_tab"] = "attachments" + if "form" not in kwargs: + kwargs["form"] = self.get_form() return super().get_context_data(**kwargs) @@ -363,7 +380,7 @@ class AttachmentSearchView(ArticleMixin, ListView): template_name = "wiki/plugins/attachments/search.html" allow_empty = True - context_object_name = 'attachments' + context_object_name = "attachments" paginator_class = WikiPaginator paginate_by = 10 @@ -372,24 +389,25 @@ class AttachmentSearchView(ArticleMixin, ListView): return super().dispatch(request, article, *args, **kwargs) def get_queryset(self): - self.query = self.request.GET.get('query', None) + self.query = self.request.GET.get("query", None) if not self.query: qs = models.Attachment.objects.none() else: qs = models.Attachment.objects.active().can_read(self.request.user) qs = qs.filter( - Q(original_filename__contains=self.query) | - Q(current_revision__description__contains=self.query) | - Q(article__current_revision__title__contains=self.query)) - return qs.order_by('original_filename') + Q(original_filename__contains=self.query) + | Q(current_revision__description__contains=self.query) + | Q(article__current_revision__title__contains=self.query) + ) + return qs.order_by("original_filename") def get_context_data(self, **kwargs): # Is this a bit of a hack? Use better inheritance? kwargs_article = ArticleMixin.get_context_data(self, **kwargs) kwargs_listview = ListView.get_context_data(self, **kwargs) - kwargs['search_form'] = forms.SearchForm(self.request.GET) - kwargs['query'] = self.query + kwargs["search_form"] = forms.SearchForm(self.request.GET) + kwargs["query"] = self.query kwargs.update(kwargs_article) kwargs.update(kwargs_listview) - kwargs['selected_tab'] = 'attachments' + kwargs["selected_tab"] = "attachments" return kwargs diff --git a/src/wiki/plugins/attachments/wiki_plugin.py b/src/wiki/plugins/attachments/wiki_plugin.py index d2d9c4f9..71395199 100644 --- a/src/wiki/plugins/attachments/wiki_plugin.py +++ b/src/wiki/plugins/attachments/wiki_plugin.py @@ -11,26 +11,26 @@ from wiki.plugins.notifications.util import truncate_title class AttachmentPlugin(BasePlugin): slug = settings.SLUG - urlpatterns = { - 'article': [re_path('', include('wiki.plugins.attachments.urls'))] - } + urlpatterns = {"article": [re_path("", include("wiki.plugins.attachments.urls"))]} - article_tab = (_('Attachments'), "fa fa-file") + article_tab = (_("Attachments"), "fa fa-file") article_view = views.AttachmentView().dispatch # List of notifications to construct signal handlers for. This # is handled inside the notifications plugin. - notifications = [{ - 'model': models.AttachmentRevision, - 'message': lambda obj: ( - _("A file was changed: %s") - if not obj.deleted - else - _("A file was deleted: %s") - ) % truncate_title(obj.get_filename()), - 'key': ARTICLE_EDIT, - 'created': True, - 'get_article': lambda obj: obj.attachment.article} + notifications = [ + { + "model": models.AttachmentRevision, + "message": lambda obj: ( + _("A file was changed: %s") + if not obj.deleted + else _("A file was deleted: %s") + ) + % truncate_title(obj.get_filename()), + "key": ARTICLE_EDIT, + "created": True, + "get_article": lambda obj: obj.attachment.article, + } ] markdown_extensions = [AttachmentExtension()] diff --git a/src/wiki/plugins/editsection/__init__.py b/src/wiki/plugins/editsection/__init__.py index 3435d0b2..28627cde 100644 --- a/src/wiki/plugins/editsection/__init__.py +++ b/src/wiki/plugins/editsection/__init__.py @@ -1 +1 @@ -default_app_config = 'wiki.plugins.editsection.apps.EditSectionConfig' +default_app_config = "wiki.plugins.editsection.apps.EditSectionConfig" diff --git a/src/wiki/plugins/editsection/apps.py b/src/wiki/plugins/editsection/apps.py index 0047f288..bc9b5185 100644 --- a/src/wiki/plugins/editsection/apps.py +++ b/src/wiki/plugins/editsection/apps.py @@ -3,6 +3,6 @@ from django.utils.translation import ugettext_lazy as _ class EditSectionConfig(AppConfig): - name = 'wiki.plugins.editsection' + name = "wiki.plugins.editsection" verbose_name = _("Wiki edit section") - label = 'wiki_editsection' + label = "wiki_editsection" diff --git a/src/wiki/plugins/editsection/markdown_extensions.py b/src/wiki/plugins/editsection/markdown_extensions.py index 00de569f..edd487af 100644 --- a/src/wiki/plugins/editsection/markdown_extensions.py +++ b/src/wiki/plugins/editsection/markdown_extensions.py @@ -11,23 +11,23 @@ from . import settings class EditSectionExtension(Extension): def __init__(self, *args, **kwargs): self.config = { - 'level': [settings.MAX_LEVEL, 'Allow to edit sections until this level'], - 'headers': None, # List of FindHeader, all headers with there positions - 'location': None, # To be extracted header - 'header_id': None, # Header text ID of the to be extracted header + "level": [settings.MAX_LEVEL, "Allow to edit sections until this level"], + "headers": None, # List of FindHeader, all headers with there positions + "location": None, # To be extracted header + "header_id": None, # Header text ID of the to be extracted header } super().__init__(**kwargs) def extendMarkdown(self, md, md_globals): ext = EditSectionProcessor(md) ext.config = self.config - md.treeprocessors.add('editsection', ext, '_end') + md.treeprocessors.add("editsection", ext, "_end") def get_header_id(header): - header_id = ''.join(w[0] for w in re.findall(r"\w+", header)) + header_id = "".join(w[0] for w in re.findall(r"\w+", header)) if not len(header_id): - return '_' + return "_" return header_id @@ -48,8 +48,11 @@ class EditSectionProcessor(Treeprocessor): # Find current position in headers cur_header += 1 - while (cur_header < len(self.headers) and - not self.headers[cur_header].sure_header and child.text != self.headers[cur_header].header): + while ( + cur_header < len(self.headers) + and not self.headers[cur_header].sure_header + and child.text != self.headers[cur_header].header + ): cur_header += 1 if cur_header >= len(self.headers): return None @@ -63,7 +66,7 @@ class EditSectionProcessor(Treeprocessor): cur_pos[level - 1] += 1 last_level = level - location = '-'.join(map(str, cur_pos)) + location = "-".join(map(str, cur_pos)) if location != self.location: continue @@ -93,29 +96,31 @@ class EditSectionProcessor(Treeprocessor): cur_pos[l] = 0 cur_pos[level - 1] += 1 last_level = level - location = '-'.join(map(str, cur_pos)) + location = "-".join(map(str, cur_pos)) header_id = get_header_id(child.text) # Insert link to allow editing this section - link = etree.SubElement(child, 'a') + link = etree.SubElement(child, "a") link.text = settings.LINK_TEXT link.attrib["class"] = "article-edit-title-link" # Build the URL url_kwargs = self.md.article.get_url_kwargs() - url_kwargs['location'] = location - url_kwargs['header'] = header_id - link.attrib["href"] = reverse('wiki:editsection', kwargs=url_kwargs) + url_kwargs["location"] = location + url_kwargs["header"] = header_id + link.attrib["href"] = reverse("wiki:editsection", kwargs=url_kwargs) def run(self, root): - self.level = self.config.get('level')[0] - self.HEADER_RE = re.compile('^h([' + ''.join(map(str, range(1, self.level + 1))) + '])') - self.headers = self.config.get('headers') + self.level = self.config.get("level")[0] + self.HEADER_RE = re.compile( + "^h([" + "".join(map(str, range(1, self.level + 1))) + "])" + ) + self.headers = self.config.get("headers") if self.headers: - self.location = self.config.get('location') - self.header_id = self.config.get('header_id') - self.config['location'] = self.locate_section(root) - self.config['headers'] = None + self.location = self.config.get("location") + self.header_id = self.config.get("header_id") + self.config["location"] = self.locate_section(root) + self.config["headers"] = None else: self.add_links(root) return root diff --git a/src/wiki/plugins/editsection/settings.py b/src/wiki/plugins/editsection/settings.py index c007df9f..fc0c30b1 100644 --- a/src/wiki/plugins/editsection/settings.py +++ b/src/wiki/plugins/editsection/settings.py @@ -1,12 +1,12 @@ from django.conf import settings as django_settings from django.utils.translation import gettext -SLUG = 'editsection' +SLUG = "editsection" #: Add "[edit]" links to all section headers till this level. By using #: these links editing only the text from the selected section is possible. -MAX_LEVEL = getattr(django_settings, 'WIKI_EDITSECTION_MAX_LEVEL', 3) +MAX_LEVEL = getattr(django_settings, "WIKI_EDITSECTION_MAX_LEVEL", 3) #: Text used for the section edit links which will appear next to section #: headers. These links allow editing only the text of one particular section. -LINK_TEXT = getattr(django_settings, 'WIKI_EDITSECTION_LINK_TEXT', gettext("[edit]")) +LINK_TEXT = getattr(django_settings, "WIKI_EDITSECTION_LINK_TEXT", gettext("[edit]")) diff --git a/src/wiki/plugins/editsection/views.py b/src/wiki/plugins/editsection/views.py index 59296344..243c0176 100644 --- a/src/wiki/plugins/editsection/views.py +++ b/src/wiki/plugins/editsection/views.py @@ -17,8 +17,7 @@ ERROR_SECTION_CHANGED = gettext_lazy( "Unable to find the selected section. The article was modified meanwhile." ) ERROR_SECTION_UNSAVED = gettext_lazy( - "Your changes must be re-applied in the new section structure of the " - "article." + "Your changes must be re-applied in the new section structure of the " "article." ) ERROR_ARTICLE_CHANGED = gettext_lazy( "Unable to find the selected section in the current article. The article " @@ -34,11 +33,14 @@ class FindHeader: headers which are filtered out later in the markdown extension. Returns: start pos header sure_header level""" - SETEXT_RE_TEXT = r'(?P<header1>.*?)\n(?P<level1>[=-])+[ ]*(\n|$)' - SETEXT_RE = re.compile(r'\n%s' % SETEXT_RE_TEXT, re.MULTILINE) - HEADER_RE = re.compile(r'((\A ?\n?|\n(?![^\n]{0,3}\w).*?\n)%s' - r'|(\A|\n)(?P<level2>#{1,6})(?P<header2>.*?)#*(\n|$))' % SETEXT_RE_TEXT, re.MULTILINE) - ATTR_RE = re.compile(r'[ ]+\{\:?([^\}\n]*)\}[ ]*$') + SETEXT_RE_TEXT = r"(?P<header1>.*?)\n(?P<level1>[=-])+[ ]*(\n|$)" + SETEXT_RE = re.compile(r"\n%s" % SETEXT_RE_TEXT, re.MULTILINE) + HEADER_RE = re.compile( + r"((\A ?\n?|\n(?![^\n]{0,3}\w).*?\n)%s" + r"|(\A|\n)(?P<level2>#{1,6})(?P<header2>.*?)#*(\n|$))" % SETEXT_RE_TEXT, + re.MULTILINE, + ) + ATTR_RE = re.compile(r"[ ]+\{\:?([^\}\n]*)\}[ ]*$") def __init__(self, text, pos): self.sure_header = False @@ -54,23 +56,23 @@ class FindHeader: self.pos = match.end() - 1 # Get level and header text of the section - token = match.group('level1') + token = match.group("level1") if token: - self.header = match.group('header1').strip() - self.start = match.start('header1') + self.header = match.group("header1").strip() + self.start = match.start("header1") else: - token = match.group('level2') - self.header = match.group('header2').strip() - self.start = match.start('level2') + token = match.group("level2") + self.header = match.group("header2").strip() + self.start = match.start("level2") self.sure_header = True # Remove attribute definitions from the header text match = self.ATTR_RE.search(self.header) if match: - self.header = self.header[:match.start()].rstrip('#').rstrip() + self.header = self.header[: match.start()].rstrip("#").rstrip() # Get level of the section - if token[0] == '=': + if token[0] == "=": self.level = 1 - elif token[0] == '-': + elif token[0] == "-": self.level = 2 else: self.level = len(token) @@ -99,43 +101,42 @@ class EditSection(EditView): for e in get_markdown_extensions(): if isinstance(e, EditSectionExtension): - e.config['headers'] = headers - e.config['location'] = self.location - e.config['header_id'] = self.header_id + e.config["headers"] = headers + e.config["location"] = self.location + e.config["header_id"] = self.header_id article_markdown(text, article) - return e.config['location'] + return e.config["location"] return None def _redirect_to_article(self): if self.urlpath: - return redirect('wiki:get', path=self.urlpath.path) - return redirect('wiki:get', article_id=self.article.id) + return redirect("wiki:get", path=self.urlpath.path) + return redirect("wiki:get", article_id=self.article.id) @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): - self.location = kwargs.pop('location', 0) - self.header_id = kwargs.pop('header', 0) + self.location = kwargs.pop("location", 0) + self.header_id = kwargs.pop("header", 0) - self.urlpath = kwargs.get('urlpath') - kwargs['path'] = self.urlpath.path + self.urlpath = kwargs.get("urlpath") + kwargs["path"] = self.urlpath.path - if request.method == 'GET': + if request.method == "GET": text = article.current_revision.content location = self.locate_section(article, text) if location: - self.orig_section = text[location[0]:location[1]] + self.orig_section = text[location[0] : location[1]] # Pass the to be used content to EditSection - kwargs['content'] = self.orig_section - request.session['editsection_content'] = self.orig_section + kwargs["content"] = self.orig_section + request.session["editsection_content"] = self.orig_section else: messages.error( - request, - " ".format(ERROR_SECTION_CHANGED, ERROR_TRY_AGAIN) + request, " ".format(ERROR_SECTION_CHANGED, ERROR_TRY_AGAIN) ) return self._redirect_to_article() else: - kwargs['content'] = request.session.get('editsection_content') - self.orig_section = kwargs.get('content') + kwargs["content"] = request.session.get("editsection_content") + self.orig_section = kwargs.get("content") return super().dispatch(request, article, *args, **kwargs) @@ -148,25 +149,31 @@ class EditSection(EditView): text = get_object_or_404( models.ArticleRevision, article=self.article, - id=self.article.current_revision.previous_revision.id).content + id=self.article.current_revision.previous_revision.id, + ).content location = self.locate_section(self.article, text) if location: - if self.orig_section != text[location[0]:location[1]]: + if self.orig_section != text[location[0] : location[1]]: messages.warning( self.request, - " ".format(ERROR_SECTION_CHANGED, ERROR_SECTION_UNSAVED, ERROR_TRY_AGAIN) + " ".format( + ERROR_SECTION_CHANGED, ERROR_SECTION_UNSAVED, ERROR_TRY_AGAIN + ), ) # Include the edited section into the complete previous article - self.article.current_revision.content = text[0:location[0]] + section + text[location[1]:] + self.article.current_revision.content = ( + text[0 : location[0]] + section + text[location[1] :] + ) self.article.current_revision.save() else: # Back to the version before replacing the article with the section - self.article.current_revision = self.article.current_revision.previous_revision + self.article.current_revision = ( + self.article.current_revision.previous_revision + ) self.article.save() messages.error( - self.request, - " ".format(ERROR_ARTICLE_CHANGED, ERROR_TRY_AGAIN) + self.request, " ".format(ERROR_ARTICLE_CHANGED, ERROR_TRY_AGAIN) ) return self._redirect_to_article() diff --git a/src/wiki/plugins/editsection/wiki_plugin.py b/src/wiki/plugins/editsection/wiki_plugin.py index 5cab89cc..c2b77413 100644 --- a/src/wiki/plugins/editsection/wiki_plugin.py +++ b/src/wiki/plugins/editsection/wiki_plugin.py @@ -9,11 +9,15 @@ from . import settings, views class EditSectionPlugin(BasePlugin): slug = settings.SLUG - urlpatterns = {'article': [ - url(r'^(?P<location>[0-9-]+)/header/(?P<header>\w+)/$', - views.EditSection.as_view(), - name='editsection'), - ]} + urlpatterns = { + "article": [ + url( + r"^(?P<location>[0-9-]+)/header/(?P<header>\w+)/$", + views.EditSection.as_view(), + name="editsection", + ), + ] + } markdown_extensions = [EditSectionExtension()] diff --git a/src/wiki/plugins/globalhistory/__init__.py b/src/wiki/plugins/globalhistory/__init__.py index 6b44e7fd..0a07d487 100644 --- a/src/wiki/plugins/globalhistory/__init__.py +++ b/src/wiki/plugins/globalhistory/__init__.py @@ -1 +1 @@ -default_app_config = 'wiki.plugins.globalhistory.apps.GlobalHistoryConfig' +default_app_config = "wiki.plugins.globalhistory.apps.GlobalHistoryConfig" diff --git a/src/wiki/plugins/globalhistory/apps.py b/src/wiki/plugins/globalhistory/apps.py index 6747fa42..765e054f 100644 --- a/src/wiki/plugins/globalhistory/apps.py +++ b/src/wiki/plugins/globalhistory/apps.py @@ -3,6 +3,6 @@ from django.utils.translation import gettext_lazy as _ class GlobalHistoryConfig(AppConfig): - name = 'wiki.plugins.globalhistory' + name = "wiki.plugins.globalhistory" verbose_name = _("Wiki Global History") - label = 'wiki_globalhistory' + label = "wiki_globalhistory" diff --git a/src/wiki/plugins/globalhistory/settings.py b/src/wiki/plugins/globalhistory/settings.py index 6cbabf35..6664bfd4 100644 --- a/src/wiki/plugins/globalhistory/settings.py +++ b/src/wiki/plugins/globalhistory/settings.py @@ -1 +1 @@ -SLUG = 'globalhistory' +SLUG = "globalhistory" diff --git a/src/wiki/plugins/globalhistory/views.py b/src/wiki/plugins/globalhistory/views.py index 2d850e5b..f81ea9f3 100644 --- a/src/wiki/plugins/globalhistory/views.py +++ b/src/wiki/plugins/globalhistory/views.py @@ -8,25 +8,27 @@ from wiki.core.paginator import WikiPaginator class GlobalHistory(ListView): - template_name = 'wiki/plugins/globalhistory/globalhistory.html' + template_name = "wiki/plugins/globalhistory/globalhistory.html" paginator_class = WikiPaginator paginate_by = 30 model = models.ArticleRevision - context_object_name = 'revisions' + context_object_name = "revisions" @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): - self.only_last = kwargs.get('only_last', 0) - return super().dispatch( - request, *args, **kwargs) + self.only_last = kwargs.get("only_last", 0) + return super().dispatch(request, *args, **kwargs) def get_queryset(self): - if self.only_last == '1': - return self.model.objects.can_read(self.request.user) \ - .filter(article__current_revision=F('id')).order_by('-modified') + if self.only_last == "1": + return ( + self.model.objects.can_read(self.request.user) + .filter(article__current_revision=F("id")) + .order_by("-modified") + ) else: - return self.model.objects.can_read(self.request.user).order_by('-modified') + return self.model.objects.can_read(self.request.user).order_by("-modified") def get_context_data(self, **kwargs): - kwargs['only_last'] = self.only_last + kwargs["only_last"] = self.only_last return super().get_context_data(**kwargs) diff --git a/src/wiki/plugins/globalhistory/wiki_plugin.py b/src/wiki/plugins/globalhistory/wiki_plugin.py index 6966ceee..c531652c 100644 --- a/src/wiki/plugins/globalhistory/wiki_plugin.py +++ b/src/wiki/plugins/globalhistory/wiki_plugin.py @@ -8,10 +8,16 @@ from . import settings, views class GlobalHistoryPlugin(BasePlugin): slug = settings.SLUG - urlpatterns = {'root': [ - re_path(r'^$', views.GlobalHistory.as_view(), name='globalhistory'), - re_path('^(?P<only_last>[01])/$', views.GlobalHistory.as_view(), name='globalhistory'), - ]} + urlpatterns = { + "root": [ + re_path(r"^$", views.GlobalHistory.as_view(), name="globalhistory"), + re_path( + "^(?P<only_last>[01])/$", + views.GlobalHistory.as_view(), + name="globalhistory", + ), + ] + } registry.register(GlobalHistoryPlugin) diff --git a/src/wiki/plugins/help/__init__.py b/src/wiki/plugins/help/__init__.py index be534e3a..65e2e77b 100644 --- a/src/wiki/plugins/help/__init__.py +++ b/src/wiki/plugins/help/__init__.py @@ -1 +1 @@ -default_app_config = 'wiki.plugins.help.apps.HelpConfig' +default_app_config = "wiki.plugins.help.apps.HelpConfig" diff --git a/src/wiki/plugins/help/apps.py b/src/wiki/plugins/help/apps.py index 5cbf12e4..4d00f603 100644 --- a/src/wiki/plugins/help/apps.py +++ b/src/wiki/plugins/help/apps.py @@ -3,6 +3,6 @@ from django.utils.translation import gettext_lazy as _ class HelpConfig(AppConfig): - name = 'wiki.plugins.help' + name = "wiki.plugins.help" verbose_name = _("Wiki help") - label = 'wiki_help' + label = "wiki_help" diff --git a/src/wiki/plugins/help/wiki_plugin.py b/src/wiki/plugins/help/wiki_plugin.py index 33fc84be..1cf3eb13 100644 --- a/src/wiki/plugins/help/wiki_plugin.py +++ b/src/wiki/plugins/help/wiki_plugin.py @@ -5,13 +5,15 @@ from wiki.core.plugins.base import BasePlugin class HelpPlugin(BasePlugin): - slug = 'help' + slug = "help" - sidebar = {'headline': _('Help'), - 'icon_class': 'fa-question-circle', - 'template': 'wiki/plugins/help/sidebar.html', - 'form_class': None, - 'get_form_kwargs': (lambda a: {})} + sidebar = { + "headline": _("Help"), + "icon_class": "fa-question-circle", + "template": "wiki/plugins/help/sidebar.html", + "form_class": None, + "get_form_kwargs": (lambda a: {}), + } markdown_extensions = [] diff --git a/src/wiki/plugins/images/__init__.py b/src/wiki/plugins/images/__init__.py index 767b8968..29059bdb 100644 --- a/src/wiki/plugins/images/__init__.py +++ b/src/wiki/plugins/images/__init__.py @@ -1 +1 @@ -default_app_config = 'wiki.plugins.images.apps.ImagesConfig' +default_app_config = "wiki.plugins.images.apps.ImagesConfig" diff --git a/src/wiki/plugins/images/admin.py b/src/wiki/plugins/images/admin.py index 38721026..ce321491 100644 --- a/src/wiki/plugins/images/admin.py +++ b/src/wiki/plugins/images/admin.py @@ -5,7 +5,6 @@ from . import models class ImageForm(forms.ModelForm): - class Meta: model = models.Image exclude = () @@ -13,19 +12,19 @@ class ImageForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance.pk: - revisions = models.ImageRevision.objects.filter( - plugin=self.instance) - self.fields['current_revision'].queryset = revisions + revisions = models.ImageRevision.objects.filter(plugin=self.instance) + self.fields["current_revision"].queryset = revisions else: self.fields[ - 'current_revision'].queryset = models.ImageRevision.objects.none() - self.fields['current_revision'].widget = forms.HiddenInput() + "current_revision" + ].queryset = models.ImageRevision.objects.none() + self.fields["current_revision"].widget = forms.HiddenInput() class ImageRevisionInline(admin.TabularInline): model = models.ImageRevision extra = 1 - fields = ('image', 'locked', 'deleted') + fields = ("image", "locked", "deleted") class ImageAdmin(admin.ModelAdmin): diff --git a/src/wiki/plugins/images/apps.py b/src/wiki/plugins/images/apps.py index 81ff64be..899a2c65 100644 --- a/src/wiki/plugins/images/apps.py +++ b/src/wiki/plugins/images/apps.py @@ -6,9 +6,12 @@ from . import checks class ImagesConfig(AppConfig): - name = 'wiki.plugins.images' + name = "wiki.plugins.images" verbose_name = _("Wiki images") - label = 'wiki_images' + label = "wiki_images" def ready(self): - register(checks.check_for_required_installed_apps, checks.Tags.required_installed_apps) + register( + checks.check_for_required_installed_apps, + checks.Tags.required_installed_apps, + ) diff --git a/src/wiki/plugins/images/checks.py b/src/wiki/plugins/images/checks.py index 774eda17..a73af165 100644 --- a/src/wiki/plugins/images/checks.py +++ b/src/wiki/plugins/images/checks.py @@ -9,5 +9,7 @@ class Tags: def check_for_required_installed_apps(app_configs, **kwargs): errors = [] if not apps.is_installed("sorl.thumbnail"): - errors.append(Error('needs sorl.thumbnail in INSTALLED_APPS', id='wiki_images.E001')) + errors.append( + Error("needs sorl.thumbnail in INSTALLED_APPS", id="wiki_images.E001") + ) return errors diff --git a/src/wiki/plugins/images/forms.py b/src/wiki/plugins/images/forms.py index ca5f9a68..72e84b8b 100644 --- a/src/wiki/plugins/images/forms.py +++ b/src/wiki/plugins/images/forms.py @@ -5,22 +5,25 @@ from wiki.plugins.images import models class SidebarForm(PluginSidebarFormMixin): - def __init__(self, article, request, *args, **kwargs): self.article = article self.request = request super().__init__(*args, **kwargs) - self.fields['image'].required = True + self.fields["image"].required = True def get_usermessage(self): - return gettext( - "New image %s was successfully uploaded. You can use it by selecting it from the list of available images.") % self.instance.get_filename() + return ( + gettext( + "New image %s was successfully uploaded. You can use it by selecting it from the list of available images." + ) + % self.instance.get_filename() + ) def save(self, *args, **kwargs): if not self.instance.id: image = models.Image() image.article = self.article - kwargs['commit'] = False + kwargs["commit"] = False revision = super().save(*args, **kwargs) revision.set_from_request(self.request) image.add_revision(self.instance, save=True) @@ -29,20 +32,19 @@ class SidebarForm(PluginSidebarFormMixin): class Meta: model = models.ImageRevision - fields = ('image',) + fields = ("image",) class RevisionForm(forms.ModelForm): - def __init__(self, *args, **kwargs): - self.image = kwargs.pop('image') - self.request = kwargs.pop('request') + self.image = kwargs.pop("image") + self.request = kwargs.pop("request") super().__init__(*args, **kwargs) - self.fields['image'].required = True + self.fields["image"].required = True def save(self, *args, **kwargs): if not self.instance.id: - kwargs['commit'] = False + kwargs["commit"] = False revision = super().save(*args, **kwargs) revision.inherit_predecessor(self.image, skip_image_file=True) revision.deleted = False # Restore automatically if deleted @@ -53,15 +55,15 @@ class RevisionForm(forms.ModelForm): class Meta: model = models.ImageRevision - fields = ('image',) + fields = ("image",) class PurgeForm(forms.Form): - confirm = forms.BooleanField(label=_('Are you sure?'), required=False) + confirm = forms.BooleanField(label=_("Are you sure?"), required=False) def clean_confirm(self): - confirm = self.cleaned_data['confirm'] + confirm = self.cleaned_data["confirm"] if not confirm: - raise forms.ValidationError(gettext('You are not sure enough!')) + raise forms.ValidationError(gettext("You are not sure enough!")) return confirm diff --git a/src/wiki/plugins/images/markdown_extensions.py b/src/wiki/plugins/images/markdown_extensions.py index b17e3957..d5b5b799 100644 --- a/src/wiki/plugins/images/markdown_extensions.py +++ b/src/wiki/plugins/images/markdown_extensions.py @@ -3,17 +3,22 @@ from django.template.loader import render_to_string from wiki.plugins.images import models, settings IMAGE_RE = ( - r"(?:(?im)" + + r"(?:(?im)" + + # Match '[image:N' - r"\[image\:(?P<id>[0-9]+)" + + r"\[image\:(?P<id>[0-9]+)" + + # Match optional 'align' - r"(?:\s+align\:(?P<align>right|left))?" + + r"(?:\s+align\:(?P<align>right|left))?" + + # Match optional 'size' - r"(?:\s+size\:(?P<size>default|small|medium|large|orig))?" + + r"(?:\s+size\:(?P<size>default|small|medium|large|orig))?" + + # Match ']' and rest of line. # Normally [^\n] could be replaced with a dot '.', since '.' # does not match newlines, but inline processors run with re.DOTALL. - r"\s*\](?P<trailer>[^\n]*)$" + + r"\s*\](?P<trailer>[^\n]*)$" + + # Match zero or more caption lines, each indented by four spaces. r"(?P<caption>(?:\n [^\n]*)*))" ) @@ -24,8 +29,8 @@ class ImageExtension(markdown.Extension): """ Images plugin markdown extension for django-wiki. """ def extendMarkdown(self, md): - md.inlinePatterns.add('dw-images', ImagePattern(IMAGE_RE, md), '>link') - md.postprocessors.add('dw-images-cleanup', ImagePostprocessor(md), '>raw_html') + md.inlinePatterns.add("dw-images", ImagePattern(IMAGE_RE, md), ">link") + md.postprocessors.add("dw-images-cleanup", ImagePostprocessor(md), ">raw_html") class ImagePattern(markdown.inlinepatterns.Pattern): @@ -61,7 +66,7 @@ class ImagePattern(markdown.inlinepatterns.Pattern): pass caption = m.group("caption") - trailer = m.group('trailer') + trailer = m.group("trailer") caption_placeholder = "{{{IMAGECAPTION}}}" width = size.split("x")[0] if size else None @@ -82,7 +87,6 @@ class ImagePattern(markdown.inlinepatterns.Pattern): class ImagePostprocessor(markdown.postprocessors.Postprocessor): - def run(self, text): """ This cleans up after Markdown's well-intended placing of image tags diff --git a/src/wiki/plugins/images/migrations/0001_initial.py b/src/wiki/plugins/images/migrations/0001_initial.py index 59cb7b94..6d85fe4d 100644 --- a/src/wiki/plugins/images/migrations/0001_initial.py +++ b/src/wiki/plugins/images/migrations/0001_initial.py @@ -5,34 +5,61 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('wiki', '0001_initial'), + ("wiki", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Image', + name="Image", fields=[ - ('revisionplugin_ptr', models.OneToOneField(to='wiki.RevisionPlugin', primary_key=True, auto_created=True, parent_link=True, serialize=False, on_delete=models.CASCADE)), + ( + "revisionplugin_ptr", + models.OneToOneField( + to="wiki.RevisionPlugin", + primary_key=True, + auto_created=True, + parent_link=True, + serialize=False, + on_delete=models.CASCADE, + ), + ), ], - options={ - 'verbose_name': 'image', - 'verbose_name_plural': 'images', - }, - bases=('wiki.revisionplugin',), + options={"verbose_name": "image", "verbose_name_plural": "images",}, + bases=("wiki.revisionplugin",), ), migrations.CreateModel( - name='ImageRevision', + name="ImageRevision", fields=[ - ('revisionpluginrevision_ptr', models.OneToOneField(to='wiki.RevisionPluginRevision', primary_key=True, auto_created=True, parent_link=True, serialize=False, on_delete=models.CASCADE)), - ('image', models.ImageField(null=True, blank=True, height_field='height', max_length=2000, width_field='width', upload_to=wiki.plugins.images.models.upload_path)), - ('width', models.SmallIntegerField(null=True, blank=True)), - ('height', models.SmallIntegerField(null=True, blank=True)), + ( + "revisionpluginrevision_ptr", + models.OneToOneField( + to="wiki.RevisionPluginRevision", + primary_key=True, + auto_created=True, + parent_link=True, + serialize=False, + on_delete=models.CASCADE, + ), + ), + ( + "image", + models.ImageField( + null=True, + blank=True, + height_field="height", + max_length=2000, + width_field="width", + upload_to=wiki.plugins.images.models.upload_path, + ), + ), + ("width", models.SmallIntegerField(null=True, blank=True)), + ("height", models.SmallIntegerField(null=True, blank=True)), ], options={ - 'verbose_name': 'image revision', - 'verbose_name_plural': 'image revisions', - 'ordering': ('-created',), + "verbose_name": "image revision", + "verbose_name_plural": "image revisions", + "ordering": ("-created",), }, - bases=('wiki.revisionpluginrevision',), + bases=("wiki.revisionpluginrevision",), ), ] diff --git a/src/wiki/plugins/images/migrations/0002_auto_20151118_1811.py b/src/wiki/plugins/images/migrations/0002_auto_20151118_1811.py index 964937a0..3c16fca4 100644 --- a/src/wiki/plugins/images/migrations/0002_auto_20151118_1811.py +++ b/src/wiki/plugins/images/migrations/0002_auto_20151118_1811.py @@ -6,16 +6,12 @@ class Migration(migrations.Migration): atomic = False dependencies = [ - ('wiki_images', '0001_initial'), + ("wiki_images", "0001_initial"), ] operations = [ + migrations.AlterModelTable(name="image", table="wiki_images_image",), migrations.AlterModelTable( - name='image', - table='wiki_images_image', - ), - migrations.AlterModelTable( - name='imagerevision', - table='wiki_images_imagerevision', + name="imagerevision", table="wiki_images_imagerevision", ), ] diff --git a/src/wiki/plugins/images/models.py b/src/wiki/plugins/images/models.py index b57dd0a0..b2ddd152 100644 --- a/src/wiki/plugins/images/models.py +++ b/src/wiki/plugins/images/models.py @@ -13,10 +13,10 @@ def upload_path(instance, filename): # Has to match original extension filename upload_path = settings.IMAGE_PATH - upload_path = upload_path.replace( - '%aid', str(instance.plugin.image.article.id)) + upload_path = upload_path.replace("%aid", str(instance.plugin.image.article.id)) if settings.IMAGE_PATH_OBSCURIFY: import uuid + upload_path = os.path.join(upload_path, uuid.uuid4().hex) return os.path.join(upload_path, filename) @@ -35,23 +35,31 @@ class Image(RevisionPlugin): return self.can_write(user) class Meta: - verbose_name = _('image') - verbose_name_plural = _('images') - db_table = 'wiki_images_image' # Matches label of upcoming 0.1 release + verbose_name = _("image") + verbose_name_plural = _("images") + db_table = "wiki_images_image" # Matches label of upcoming 0.1 release def __str__(self): if self.current_revision: - return gettext('Image: %s') % self.current_revision.imagerevision.get_filename() + return ( + gettext("Image: %s") + % self.current_revision.imagerevision.get_filename() + ) else: - return gettext('Current revision not set!!') + return gettext("Current revision not set!!") class ImageRevision(RevisionPluginRevision): - image = models.ImageField(upload_to=upload_path, - max_length=2000, height_field='height', - width_field='width', blank=True, null=True, - storage=settings.STORAGE_BACKEND) + image = models.ImageField( + upload_to=upload_path, + max_length=2000, + height_field="height", + width_field="width", + blank=True, + null=True, + storage=settings.STORAGE_BACKEND, + ) width = models.SmallIntegerField(blank=True, null=True) height = models.SmallIntegerField(blank=True, null=True) @@ -59,7 +67,7 @@ class ImageRevision(RevisionPluginRevision): def get_filename(self): if self.image: try: - return self.image.name.split('/')[-1] + return self.image.name.split("/")[-1] except OSError: pass return None @@ -94,17 +102,17 @@ class ImageRevision(RevisionPluginRevision): self.image = None class Meta: - verbose_name = _('image revision') - verbose_name_plural = _('image revisions') + verbose_name = _("image revision") + verbose_name_plural = _("image revisions") # Matches label of upcoming 0.1 release - db_table = 'wiki_images_imagerevision' - ordering = ('-created',) + db_table = "wiki_images_imagerevision" + ordering = ("-created",) def __str__(self): if self.revision_number: - return gettext('Image Revision: %d') % self.revision_number + return gettext("Image Revision: %d") % self.revision_number else: - return gettext('Current revision not set!!') + return gettext("Current revision not set!!") def on_image_revision_delete(instance, *args, **kwargs): @@ -117,7 +125,7 @@ def on_image_revision_delete(instance, *args, **kwargs): try: path = instance.image.path.split("/")[:-1] except NotImplementedError: - # This backend storage doesn't implement 'path' so there is no path to delete + # This backend storage doesn't implement 'path' so there is no path to delete return # Clean up empty directories @@ -131,8 +139,7 @@ def on_image_revision_delete(instance, *args, **kwargs): for depth in range(0, max_depth): delete_path = "/".join(path[:-depth] if depth > 0 else path) try: - dir_list = os.listdir( - os.path.join(django_settings.MEDIA_ROOT, delete_path)) + dir_list = os.listdir(os.path.join(django_settings.MEDIA_ROOT, delete_path)) except OSError: # Path does not exist, so let's not try to remove it... dir_list = None diff --git a/src/wiki/plugins/images/settings.py b/src/wiki/plugins/images/settings.py index 17177e92..78622b21 100644 --- a/src/wiki/plugins/images/settings.py +++ b/src/wiki/plugins/images/settings.py @@ -1,46 +1,45 @@ from django.conf import settings as django_settings from wiki.conf import settings as wiki_settings -SLUG = 'images' +SLUG = "images" # Deprecated APP_LABEL = None #: Location where uploaded images are stored. ``%aid`` is replaced by the article id. -IMAGE_PATH = getattr(django_settings, 'WIKI_IMAGES_PATH', "wiki/images/%aid/") +IMAGE_PATH = getattr(django_settings, "WIKI_IMAGES_PATH", "wiki/images/%aid/") #: Size for the image thumbnail included in the HTML text. If no specific #: size is given in the markdown tag the ``default`` size is used. If a #: specific size is given in the markdown tag that size is used. -THUMBNAIL_SIZES = getattr(django_settings, 'WIKI_IMAGES_THUMBNAIL_SIZES', { - 'default': '250x250', - 'small': '150x150', - 'medium': '300x300', - 'large': '500x500', - 'orig': None -}) +THUMBNAIL_SIZES = getattr( + django_settings, + "WIKI_IMAGES_THUMBNAIL_SIZES", + { + "default": "250x250", + "small": "150x150", + "medium": "300x300", + "large": "500x500", + "orig": None, + }, +) #: Storage backend to use, default is to use the same as the rest of the #: wiki, which is set in ``WIKI_STORAGE_BACKEND``, but you can override it #: with ``WIKI_IMAGES_STORAGE_BACKEND``. STORAGE_BACKEND = getattr( - django_settings, - 'WIKI_IMAGES_STORAGE_BACKEND', - wiki_settings.STORAGE_BACKEND) + django_settings, "WIKI_IMAGES_STORAGE_BACKEND", wiki_settings.STORAGE_BACKEND +) #: Should the upload path be obscurified? If so, a random hash will be added #: to the path such that someone can not guess the location of files (if you #: have restricted permissions and the files are still located within the #: web server's file system). -IMAGE_PATH_OBSCURIFY = getattr( - django_settings, - 'WIKI_IMAGES_PATH_OBSCURIFY', - True) +IMAGE_PATH_OBSCURIFY = getattr(django_settings, "WIKI_IMAGES_PATH_OBSCURIFY", True) #: Allow anonymous users upload access (not nice on an open network). #: ``WIKI_IMAGES_ANONYMOUS`` can override this, otherwise the default #: in ``wiki.conf.settings`` is used. ANONYMOUS = getattr( - django_settings, - 'WIKI_IMAGES_ANONYMOUS', - wiki_settings.ANONYMOUS_UPLOAD) + django_settings, "WIKI_IMAGES_ANONYMOUS", wiki_settings.ANONYMOUS_UPLOAD +) diff --git a/src/wiki/plugins/images/templatetags/wiki_images_tags.py b/src/wiki/plugins/images/templatetags/wiki_images_tags.py index 612aaf1c..23945f82 100644 --- a/src/wiki/plugins/images/templatetags/wiki_images_tags.py +++ b/src/wiki/plugins/images/templatetags/wiki_images_tags.py @@ -7,8 +7,8 @@ register = template.Library() @register.filter def images_for_article(article): return models.Image.objects.filter( - article=article, current_revision__deleted=False).order_by( - '-current_revision__created') + article=article, current_revision__deleted=False + ).order_by("-current_revision__created") @register.filter diff --git a/src/wiki/plugins/images/views.py b/src/wiki/plugins/images/views.py index 81144421..a302a9c7 100644 --- a/src/wiki/plugins/images/views.py +++ b/src/wiki/plugins/images/views.py @@ -18,9 +18,9 @@ logger = logging.getLogger(__name__) class ImageView(ArticleMixin, ListView): - template_name = 'wiki/plugins/images/index.html' + template_name = "wiki/plugins/images/index.html" allow_empty = True - context_object_name = 'images' + context_object_name = "images" paginator_class = WikiPaginator paginate_by = 10 @@ -29,15 +29,16 @@ class ImageView(ArticleMixin, ListView): return super().dispatch(request, article, *args, **kwargs) def get_queryset(self): - if (self.article.can_moderate(self.request.user) or - self.article.can_delete(self.request.user)): + if self.article.can_moderate(self.request.user) or self.article.can_delete( + self.request.user + ): images = models.Image.objects.filter(article=self.article) else: images = models.Image.objects.filter( - article=self.article, - current_revision__deleted=False) + article=self.article, current_revision__deleted=False + ) images.select_related() - return images.order_by('-current_revision__imagerevision__created') + return images.order_by("-current_revision__imagerevision__created") def get_context_data(self, **kwargs): kwargs.update(ArticleMixin.get_context_data(self, **kwargs)) @@ -50,35 +51,47 @@ class DeleteView(ArticleMixin, RedirectView): @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): - self.image = get_object_or_404(models.Image, article=article, - id=kwargs.get('image_id', None)) - self.restore = kwargs.get('restore', False) + self.image = get_object_or_404( + models.Image, article=article, id=kwargs.get("image_id", None) + ) + self.restore = kwargs.get("restore", False) return ArticleMixin.dispatch(self, request, article, *args, **kwargs) def get_redirect_url(self, **kwargs): if not self.image.current_revision: - logger.critical('Encountered an image without current revision set, ID: {}'.format(self.image.id)) + logger.critical( + "Encountered an image without current revision set, ID: {}".format( + self.image.id + ) + ) latest_revision = RevisionPluginRevision.objects.filter( plugin=self.image - ).latest('pk') + ).latest("pk") self.image.current_revision = latest_revision new_revision = models.ImageRevision() new_revision.inherit_predecessor(self.image) new_revision.set_from_request(self.request) - new_revision.revision_number = RevisionPluginRevision.objects.filter(plugin=self.image).count() + new_revision.revision_number = RevisionPluginRevision.objects.filter( + plugin=self.image + ).count() new_revision.deleted = not self.restore new_revision.save() self.image.current_revision = new_revision self.image.save() if self.restore: - messages.info(self.request, _('%s has been restored') % new_revision.get_filename()) + messages.info( + self.request, _("%s has been restored") % new_revision.get_filename() + ) else: - messages.info(self.request, _('%s has been marked as deleted') % new_revision.get_filename()) + messages.info( + self.request, + _("%s has been marked as deleted") % new_revision.get_filename(), + ) if self.urlpath: - return reverse('wiki:images_index', kwargs={'path': self.urlpath.path}) - return reverse('wiki:images_index', kwargs={'article_id': self.article.id}) + return reverse("wiki:images_index", kwargs={"path": self.urlpath.path}) + return reverse("wiki:images_index", kwargs={"article_id": self.article.id}) class PurgeView(ArticleMixin, FormView): @@ -89,8 +102,9 @@ class PurgeView(ArticleMixin, FormView): @method_decorator(get_article(can_write=True, can_moderate=True)) def dispatch(self, request, article, *args, **kwargs): - self.image = get_object_or_404(models.Image, article=article, - id=kwargs.get('image_id', None)) + self.image = get_object_or_404( + models.Image, article=article, id=kwargs.get("image_id", None) + ) return super().dispatch(request, article, *args, **kwargs) def form_valid(self, form): @@ -100,14 +114,14 @@ class PurgeView(ArticleMixin, FormView): revision.imagerevision.delete() if self.urlpath: - return redirect('wiki:images_index', path=self.urlpath.path) - return redirect('wiki:images_index', article_id=self.article_id) + return redirect("wiki:images_index", path=self.urlpath.path) + return redirect("wiki:images_index", article_id=self.article_id) def get_context_data(self, **kwargs): # Needed since Django 1.9 because get_context_data is no longer called # with the form instance - if 'form' not in kwargs: - kwargs['form'] = self.get_form() + if "form" not in kwargs: + kwargs["form"] = self.get_form() kwargs = ArticleMixin.get_context_data(self, **kwargs) kwargs.update(FormView.get_context_data(self, **kwargs)) return kwargs @@ -119,14 +133,12 @@ class RevisionChangeView(ArticleMixin, RedirectView): @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): - self.image = get_object_or_404(models.Image, article=article, - id=kwargs.get('image_id', None)) + self.image = get_object_or_404( + models.Image, article=article, id=kwargs.get("image_id", None) + ) self.revision = get_object_or_404( - models.ImageRevision, - plugin__article=article, - id=kwargs.get( - 'rev_id', - None)) + models.ImageRevision, plugin__article=article, id=kwargs.get("rev_id", None) + ) return ArticleMixin.dispatch(self, request, article, *args, **kwargs) def get_redirect_url(self, **kwargs): @@ -135,18 +147,15 @@ class RevisionChangeView(ArticleMixin, RedirectView): self.image.save() messages.info( self.request, - _('%(file)s has been changed to revision #%(revision)d') % { - 'file': self.image.current_revision.imagerevision.get_filename(), - 'revision': self.revision.revision_number}) + _("%(file)s has been changed to revision #%(revision)d") + % { + "file": self.image.current_revision.imagerevision.get_filename(), + "revision": self.revision.revision_number, + }, + ) if self.urlpath: - return reverse( - 'wiki:images_index', - kwargs={ - 'path': self.urlpath.path}) - return reverse( - 'wiki:images_index', - kwargs={ - 'article_id': self.article.id}) + return reverse("wiki:images_index", kwargs={"path": self.urlpath.path}) + return reverse("wiki:images_index", kwargs={"article_id": self.article.id}) class RevisionAddView(ArticleMixin, FormView): @@ -156,32 +165,35 @@ class RevisionAddView(ArticleMixin, FormView): @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): - self.image = get_object_or_404(models.Image, article=article, - id=kwargs.get('image_id', None)) + self.image = get_object_or_404( + models.Image, article=article, id=kwargs.get("image_id", None) + ) if not self.image.can_write(request.user): return redirect(wiki_settings.LOGIN_URL) return ArticleMixin.dispatch(self, request, article, *args, **kwargs) def get_form_kwargs(self, **kwargs): kwargs = super().get_form_kwargs(**kwargs) - kwargs['image'] = self.image - kwargs['request'] = self.request + kwargs["image"] = self.image + kwargs["request"] = self.request return kwargs def get_context_data(self, **kwargs): # Needed since Django 1.9 because get_context_data is no longer called # with the form instance - if 'form' not in kwargs: - kwargs['form'] = self.get_form() + if "form" not in kwargs: + kwargs["form"] = self.get_form() kwargs = super().get_context_data(**kwargs) - kwargs['image'] = self.image + kwargs["image"] = self.image return kwargs def form_valid(self, form, **kwargs): form.save() messages.info( - self.request, _('%(file)s has been saved.') % - {'file': self.image.current_revision.imagerevision.get_filename(), }) + self.request, + _("%(file)s has been saved.") + % {"file": self.image.current_revision.imagerevision.get_filename(),}, + ) if self.urlpath: - return redirect('wiki:edit', path=self.urlpath.path) - return redirect('wiki:edit', article_id=self.article.id) + return redirect("wiki:edit", path=self.urlpath.path) + return redirect("wiki:edit", article_id=self.article.id) diff --git a/src/wiki/plugins/images/wiki_plugin.py b/src/wiki/plugins/images/wiki_plugin.py index 1e79c71e..d8d50d25 100644 --- a/src/wiki/plugins/images/wiki_plugin.py +++ b/src/wiki/plugins/images/wiki_plugin.py @@ -12,56 +12,67 @@ class ImagePlugin(BasePlugin): slug = settings.SLUG sidebar = { - 'headline': _('Images'), - 'icon_class': 'fa-picture-o', - 'template': 'wiki/plugins/images/sidebar.html', - 'form_class': forms.SidebarForm, - 'get_form_kwargs': (lambda a: {'instance': models.Image(article=a)}) + "headline": _("Images"), + "icon_class": "fa-picture-o", + "template": "wiki/plugins/images/sidebar.html", + "form_class": forms.SidebarForm, + "get_form_kwargs": (lambda a: {"instance": models.Image(article=a)}), } # List of notifications to construct signal handlers for. This # is handled inside the notifications plugin. notifications = [ - {'model': models.ImageRevision, - 'message': lambda obj: _("An image was added: %s") % truncate_title(obj.get_filename()), - 'key': ARTICLE_EDIT, - 'created': False, - # Ignore if there is a previous revision... the image isn't new - 'ignore': lambda revision: bool(revision.previous_revision), - 'get_article': lambda obj: obj.article} + { + "model": models.ImageRevision, + "message": lambda obj: _("An image was added: %s") + % truncate_title(obj.get_filename()), + "key": ARTICLE_EDIT, + "created": False, + # Ignore if there is a previous revision... the image isn't new + "ignore": lambda revision: bool(revision.previous_revision), + "get_article": lambda obj: obj.article, + } ] class RenderMedia: js = [ - 'wiki/colorbox/jquery.colorbox-min.js', - 'wiki/js/images.js', + "wiki/colorbox/jquery.colorbox-min.js", + "wiki/js/images.js", ] - css = { - 'screen': 'wiki/colorbox/example1/colorbox.css' - } + css = {"screen": "wiki/colorbox/example1/colorbox.css"} - urlpatterns = {'article': [ - re_path('^$', - views.ImageView.as_view(), - name='images_index'), - re_path('^delete/(?P<image_id>[0-9]+)/$', - views.DeleteView.as_view(), - name='images_delete'), - re_path('^restore/(?P<image_id>[0-9]+)/$', - views.DeleteView.as_view(), - name='images_restore', - kwargs={'restore': True}), - re_path('^purge/(?P<image_id>[0-9]+)/$', - views.PurgeView.as_view(), - name='images_purge'), - re_path('^(?P<image_id>[0-9]+)/revision/change/(?P<rev_id>[0-9]+)/$', - views.RevisionChangeView.as_view(), - name='images_set_revision'), - re_path('^(?P<image_id>[0-9]+)/revision/add/$', - views.RevisionAddView.as_view(), - name='images_add_revision'), - ]} + urlpatterns = { + "article": [ + re_path("^$", views.ImageView.as_view(), name="images_index"), + re_path( + "^delete/(?P<image_id>[0-9]+)/$", + views.DeleteView.as_view(), + name="images_delete", + ), + re_path( + "^restore/(?P<image_id>[0-9]+)/$", + views.DeleteView.as_view(), + name="images_restore", + kwargs={"restore": True}, + ), + re_path( + "^purge/(?P<image_id>[0-9]+)/$", + views.PurgeView.as_view(), + name="images_purge", + ), + re_path( + "^(?P<image_id>[0-9]+)/revision/change/(?P<rev_id>[0-9]+)/$", + views.RevisionChangeView.as_view(), + name="images_set_revision", + ), + re_path( + "^(?P<image_id>[0-9]+)/revision/add/$", + views.RevisionAddView.as_view(), + name="images_add_revision", + ), + ] + } markdown_extensions = [ImageExtension()] diff --git a/src/wiki/plugins/links/__init__.py b/src/wiki/plugins/links/__init__.py index 4bb78d59..50e9cc09 100644 --- a/src/wiki/plugins/links/__init__.py +++ b/src/wiki/plugins/links/__init__.py @@ -1 +1 @@ -default_app_config = 'wiki.plugins.links.apps.LinksConfig' +default_app_config = "wiki.plugins.links.apps.LinksConfig" diff --git a/src/wiki/plugins/links/apps.py b/src/wiki/plugins/links/apps.py index 8b776ecf..4a03b260 100644 --- a/src/wiki/plugins/links/apps.py +++ b/src/wiki/plugins/links/apps.py @@ -3,6 +3,6 @@ from django.utils.translation import gettext_lazy as _ class LinksConfig(AppConfig): - name = 'wiki.plugins.links' + name = "wiki.plugins.links" verbose_name = _("Wiki links") - label = 'wiki_links' + label = "wiki_links" diff --git a/src/wiki/plugins/links/mdx/djangowikilinks.py b/src/wiki/plugins/links/mdx/djangowikilinks.py index b211f22a..781d7ecf 100755 --- a/src/wiki/plugins/links/mdx/djangowikilinks.py +++ b/src/wiki/plugins/links/mdx/djangowikilinks.py @@ -24,19 +24,16 @@ from wiki import models class WikiPathExtension(markdown.extensions.Extension): - def __init__(self, configs): # set extension defaults self.config = { - 'base_url': [ - '/', - 'String to append to beginning of URL.'], - 'html_class': [ - 'wikipath', - 'CSS hook. Leave blank for none.'], - 'default_level': [ + "base_url": ["/", "String to append to beginning of URL."], + "html_class": ["wikipath", "CSS hook. Leave blank for none."], + "default_level": [ 2, - 'The level that most articles are created at. Relative links will tend to start at that level.']} + "The level that most articles are created at. Relative links will tend to start at that level.", + ], + } # Override defaults with user settings for key, value in configs: @@ -46,20 +43,19 @@ class WikiPathExtension(markdown.extensions.Extension): self.md = md # append to end of inline patterns - WIKI_RE = r'\[(?P<label>[^\]]+?)\]\(wiki:(?P<wikipath>[a-zA-Z0-9\./_-]*?)(?P<fragment>#[a-zA-Z0-9\./_-]*)?\)' + WIKI_RE = r"\[(?P<label>[^\]]+?)\]\(wiki:(?P<wikipath>[a-zA-Z0-9\./_-]*?)(?P<fragment>#[a-zA-Z0-9\./_-]*)?\)" wikiPathPattern = WikiPath(WIKI_RE, self.config, md=md) wikiPathPattern.md = md - md.inlinePatterns.add('djangowikipath', wikiPathPattern, "<reference") + md.inlinePatterns.add("djangowikipath", wikiPathPattern, "<reference") class WikiPath(markdown.inlinepatterns.Pattern): - def __init__(self, pattern, config, **kwargs): super().__init__(pattern, **kwargs) self.config = config def handleMatch(self, m): - wiki_path = m.group('wikipath') + wiki_path = m.group("wikipath") absolute = False if wiki_path.startswith("/"): absolute = True @@ -71,7 +67,7 @@ class WikiPath(markdown.inlinepatterns.Pattern): path_from_link = "" if absolute: - base_path = self.config['base_url'][0] + base_path = self.config["base_url"][0] path_from_link = os_path.join(str(base_path), wiki_path) urlpath = None @@ -87,15 +83,14 @@ class WikiPath(markdown.inlinepatterns.Pattern): # We take the first (self.config['default_level'] - 1) components, so adding # one more component would make a path of length # self.config['default_level'] - starting_level = max(0, self.config['default_level'][0] - 1) - starting_path = "/".join(source_components[: starting_level]) + starting_level = max(0, self.config["default_level"][0] - 1) + starting_path = "/".join(source_components[:starting_level]) path_from_link = os_path.join(starting_path, wiki_path) lookup = models.URLPath.objects.none() if urlpath.parent: - lookup = urlpath.parent.get_descendants().filter( - slug=wiki_path) + lookup = urlpath.parent.get_descendants().filter(slug=wiki_path) else: lookup = urlpath.get_descendants().filter(slug=wiki_path) @@ -104,30 +99,30 @@ class WikiPath(markdown.inlinepatterns.Pattern): path = urlpath.get_absolute_url() else: urlpath = None - path = self.config['base_url'][0] + path_from_link + path = self.config["base_url"][0] + path_from_link - label = m.group('label') - fragment = m.group('fragment') or "" + label = m.group("label") + fragment = m.group("fragment") or "" - a = etree.Element('a') - a.set('href', path + fragment) + a = etree.Element("a") + a.set("href", path + fragment) if not urlpath: - a.set('class', self.config['html_class'][0] + " linknotfound") + a.set("class", self.config["html_class"][0] + " linknotfound") else: - a.set('class', self.config['html_class'][0]) + a.set("class", self.config["html_class"][0]) a.text = label return a def _getMeta(self): """ Return meta data or config data. """ - base_url = self.config['base_url'][0] - html_class = self.config['html_class'][0] - if hasattr(self.md, 'Meta'): - if 'wiki_base_url' in self.md.Meta: - base_url = self.md.Meta['wiki_base_url'][0] - if 'wiki_html_class' in self.md.Meta: - html_class = self.md.Meta['wiki_html_class'][0] + base_url = self.config["base_url"][0] + html_class = self.config["html_class"][0] + if hasattr(self.md, "Meta"): + if "wiki_base_url" in self.md.Meta: + base_url = self.md.Meta["wiki_base_url"][0] + if "wiki_html_class" in self.md.Meta: + html_class = self.md.Meta["wiki_html_class"][0] return base_url, html_class diff --git a/src/wiki/plugins/links/mdx/urlize.py b/src/wiki/plugins/links/mdx/urlize.py index 672a15bd..11691061 100644 --- a/src/wiki/plugins/links/mdx/urlize.py +++ b/src/wiki/plugins/links/mdx/urlize.py @@ -62,40 +62,31 @@ import markdown URLIZE_RE = ( # Links must start at beginning of string, or be preceded with # whitespace, '(', or '<'. - r'^(?P<begin>|.*?[\s\(\<])' - - r'(?P<url>' # begin url group - + r"^(?P<begin>|.*?[\s\(\<])" + r"(?P<url>" # begin url group # Leading protocol specification. - r'(?P<protocol>([A-Z][A-Z0-9+.-]*://|))' - + r"(?P<protocol>([A-Z][A-Z0-9+.-]*://|))" # Host identifier - r'(?P<host>' # begin host identifier group - - r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|' # IPv4, match before FQDN - r'\[?([A-F0-9]{1,4}:){7}([A-F0-9]{1,4})\]?|' # IPv6, full form - r'\[?:(:[A-F0-9]{1,4}){1,6}\]?|' # IPv6, leading zeros removed - r'([A-F0-9]{1,4}:){1,6}:([A-F0-9]{1,4}){1,6}|' # IPv6, zeros in middle removed. - r'\[?([A-F0-9]{1,4}:){1,6}:\]?|' # IPv6, trailing zeros removed - r'\[?::\]?|' # IPv6, just "empty" address - r'([A-Z0-9]([A-Z0-9-]{0,61}[A-Z0-9])?\.)+([A-Z]{2,6}\.?|[A-Z]{2,}\.?)' # FQDN - r')' # end host identifier group - + r"(?P<host>" # begin host identifier group + r"[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|" # IPv4, match before FQDN + r"\[?([A-F0-9]{1,4}:){7}([A-F0-9]{1,4})\]?|" # IPv6, full form + r"\[?:(:[A-F0-9]{1,4}){1,6}\]?|" # IPv6, leading zeros removed + r"([A-F0-9]{1,4}:){1,6}:([A-F0-9]{1,4}){1,6}|" # IPv6, zeros in middle removed. + r"\[?([A-F0-9]{1,4}:){1,6}:\]?|" # IPv6, trailing zeros removed + r"\[?::\]?|" # IPv6, just "empty" address + r"([A-Z0-9]([A-Z0-9-]{0,61}[A-Z0-9])?\.)+([A-Z]{2,6}\.?|[A-Z]{2,}\.?)" # FQDN + r")" # end host identifier group # Optional port - r'(:(?P<port>[0-9]+))?' - + r"(:(?P<port>[0-9]+))?" # Optional trailing slash with path and GET parameters. - r'(/(?P<path>[^\s\[\(\]\)\<\>]*))?' - - r')' # end url group - + r"(/(?P<path>[^\s\[\(\]\)\<\>]*))?" + r")" # end url group # Links must stop at end of string, or be followed by a whitespace, ')', or '>'. - r'(?P<end>[\s\)\>].*?|)$' + r"(?P<end>[\s\)\>].*?|)$" ) class UrlizePattern(markdown.inlinepatterns.Pattern): - def getCompiledRegExp(self): """ Return compiled regular expression for matching the URL @@ -111,41 +102,45 @@ class UrlizePattern(markdown.inlinepatterns.Pattern): Processes match found within the text. """ - protocol = m.group('protocol') + protocol = m.group("protocol") - url = m.group('url') + url = m.group("url") text = url - begin_url = m.group('begin') - end_url = m.group('end') + begin_url = m.group("begin") + end_url = m.group("end") # If opening and ending character for URL are not the same, # return text unchanged. if begin_url: begin_delimeter = begin_url[-1] else: - begin_delimeter = '' + begin_delimeter = "" if end_url: end_delimeter = end_url[0] else: - end_delimeter = '' + end_delimeter = "" if ( - begin_delimeter == '<' and end_delimeter != '>' or - begin_delimeter == '(' and end_delimeter != ')' or - end_delimeter == ')' and begin_delimeter != '(' or - end_delimeter == '>' and begin_delimeter != '<' + begin_delimeter == "<" + and end_delimeter != ">" + or begin_delimeter == "(" + and end_delimeter != ")" + or end_delimeter == ")" + and begin_delimeter != "(" + or end_delimeter == ">" + and begin_delimeter != "<" ): return url # If no supported protocol is specified, assume plaintext http # and add it to the url. - if protocol == '': - url = 'http://' + url + if protocol == "": + url = "http://" + url # Convenience link to distinguish external links more easily. icon = markdown.util.etree.Element("span") - icon.set('class', 'fa fa-external-link') + icon.set("class", "fa fa-external-link") # Link text. span_text = markdown.util.etree.Element("span") @@ -153,9 +148,9 @@ class UrlizePattern(markdown.inlinepatterns.Pattern): # Set-up link itself. el = markdown.util.etree.Element("a") - el.set('href', url) - el.set('target', '_blank') - el.set('rel', 'nofollow') + el.set("href", url) + el.set("target", "_blank") + el.set("rel", "nofollow") el.append(icon) el.append(span_text) @@ -170,7 +165,7 @@ class UrlizeExtension(markdown.extensions.Extension): def extendMarkdown(self, md): """ Replace autolink with UrlizePattern """ - md.inlinePatterns['autolink'] = UrlizePattern(URLIZE_RE, md) + md.inlinePatterns["autolink"] = UrlizePattern(URLIZE_RE, md) def makeExtension(*args, **kwargs): diff --git a/src/wiki/plugins/links/settings.py b/src/wiki/plugins/links/settings.py index 28d22dd3..9e68193d 100644 --- a/src/wiki/plugins/links/settings.py +++ b/src/wiki/plugins/links/settings.py @@ -6,4 +6,4 @@ from django.conf import settings as django_settings #: can be done by following the link. This link will be relative to #: ``LOOKUP_LEVEL``. This should be the level that most articles are #: created at. -LOOKUP_LEVEL = getattr(django_settings, 'WIKI_LINKS_LOOKUP_LEVEL', 2) +LOOKUP_LEVEL = getattr(django_settings, "WIKI_LINKS_LOOKUP_LEVEL", 2) diff --git a/src/wiki/plugins/links/views.py b/src/wiki/plugins/links/views.py index 2ecd6ebe..616daf49 100644 --- a/src/wiki/plugins/links/views.py +++ b/src/wiki/plugins/links/views.py @@ -6,26 +6,28 @@ from wiki.decorators import get_article class QueryUrlPath(View): - @method_decorator(get_article(can_read=True)) def dispatch(self, request, article, *args, **kwargs): - max_num = kwargs.pop('max_num', 20) - query = request.GET.get('query', None) + max_num = kwargs.pop("max_num", 20) + query = request.GET.get("query", None) matches = [] if query: - matches = models.URLPath.objects.can_read( - request.user).active().filter( - article__current_revision__title__contains=query, - article__current_revision__deleted=False, + matches = ( + models.URLPath.objects.can_read(request.user) + .active() + .filter( + article__current_revision__title__contains=query, + article__current_revision__deleted=False, + ) ) matches = matches.select_related_common() matches = [ "[{title:s}](wiki:{url:s})".format( - title=m.article.current_revision.title, - url='/' + m.path.strip("/") - ) for m in matches[:max_num] + title=m.article.current_revision.title, url="/" + m.path.strip("/") + ) + for m in matches[:max_num] ] return object_to_json_response(matches) diff --git a/src/wiki/plugins/links/wiki_plugin.py b/src/wiki/plugins/links/wiki_plugin.py index 48ba5278..b268efdb 100644 --- a/src/wiki/plugins/links/wiki_plugin.py +++ b/src/wiki/plugins/links/wiki_plugin.py @@ -9,27 +9,31 @@ from wiki.plugins.links.mdx.urlize import makeExtension as urlize_makeExtension class LinkPlugin(BasePlugin): - slug = 'links' - urlpatterns = {'article': [ - re_path(r'^json/query-urlpath/$', - views.QueryUrlPath.as_view(), - name='links_query_urlpath'), - ]} - - sidebar = {'headline': _('Links'), - 'icon_class': 'fa-bookmark', - 'template': 'wiki/plugins/links/sidebar.html', - 'form_class': None, - 'get_form_kwargs': (lambda a: {})} + slug = "links" + urlpatterns = { + "article": [ + re_path( + r"^json/query-urlpath/$", + views.QueryUrlPath.as_view(), + name="links_query_urlpath", + ), + ] + } + + sidebar = { + "headline": _("Links"), + "icon_class": "fa-bookmark", + "template": "wiki/plugins/links/sidebar.html", + "form_class": None, + "get_form_kwargs": (lambda a: {}), + } wikipath_config = [ - ('base_url', reverse_lazy('wiki:get', kwargs={'path': ''})), - ('default_level', settings.LOOKUP_LEVEL), + ("base_url", reverse_lazy("wiki:get", kwargs={"path": ""})), + ("default_level", settings.LOOKUP_LEVEL), ] - markdown_extensions = [ - urlize_makeExtension(), - WikiPathExtension(wikipath_config)] + markdown_extensions = [urlize_makeExtension(), WikiPathExtension(wikipath_config)] registry.register(LinkPlugin) diff --git a/src/wiki/plugins/macros/__init__.py b/src/wiki/plugins/macros/__init__.py index ae663873..4b7e4785 100644 --- a/src/wiki/plugins/macros/__init__.py +++ b/src/wiki/plugins/macros/__init__.py @@ -1 +1 @@ -default_app_config = 'wiki.plugins.macros.apps.MacrosConfig' +default_app_config = "wiki.plugins.macros.apps.MacrosConfig" diff --git a/src/wiki/plugins/macros/apps.py b/src/wiki/plugins/macros/apps.py index b909213a..9cc8efeb 100644 --- a/src/wiki/plugins/macros/apps.py +++ b/src/wiki/plugins/macros/apps.py @@ -3,6 +3,6 @@ from django.utils.translation import gettext_lazy as _ class MacrosConfig(AppConfig): - name = 'wiki.plugins.macros' + name = "wiki.plugins.macros" verbose_name = _("Wiki macros") - label = 'wiki_macros' + label = "wiki_macros" diff --git a/src/wiki/plugins/macros/mdx/macro.py b/src/wiki/plugins/macros/mdx/macro.py index 81b2066a..88246e48 100644 --- a/src/wiki/plugins/macros/mdx/macro.py +++ b/src/wiki/plugins/macros/mdx/macro.py @@ -11,9 +11,8 @@ re_sq_short = r"'([^'\\]*(?:\\.[^'\\]*)*)'" MACRO_RE = r"((?i)\[(?P<macro>\w+)(?P<kwargs>\s\w+\:.+)*\])" KWARG_RE = re.compile( - r'\s*(?P<arg>\w+)(:(?P<value>([^\']+|%s)))?' % - re_sq_short, - re.IGNORECASE) + r"\s*(?P<arg>\w+)(:(?P<value>([^\']+|%s)))?" % re_sq_short, re.IGNORECASE +) class MacroExtension(markdown.Extension): @@ -21,7 +20,7 @@ class MacroExtension(markdown.Extension): """ Macro plugin markdown extension for django-wiki. """ def extendMarkdown(self, md): - md.inlinePatterns.add('dw-macros', MacroPattern(MACRO_RE, md), '>link') + md.inlinePatterns.add("dw-macros", MacroPattern(MACRO_RE, md), ">link") class MacroPattern(markdown.inlinepatterns.Pattern): @@ -30,17 +29,17 @@ class MacroPattern(markdown.inlinepatterns.Pattern): [some_macro (kw:arg)*] references. """ def handleMatch(self, m): - macro = m.group('macro').strip() + macro = m.group("macro").strip() if macro not in settings.METHODS or not hasattr(self, macro): return m.group(2) - kwargs = m.group('kwargs') + kwargs = m.group("kwargs") if not kwargs: return getattr(self, macro)() kwargs_dict = {} for kwarg in KWARG_RE.finditer(kwargs): - arg = kwarg.group('arg') - value = kwarg.group('value') + arg = kwarg.group("arg") + value = kwarg.group("value") if value is None: value = True if isinstance(value, str): @@ -58,35 +57,40 @@ class MacroPattern(markdown.inlinepatterns.Pattern): html = render_to_string( "wiki/plugins/macros/article_list.html", context={ - 'article_children': self.markdown.article.get_children( - article__current_revision__deleted=False), - 'depth': int(depth) + 1, - }) + "article_children": self.markdown.article.get_children( + article__current_revision__deleted=False + ), + "depth": int(depth) + 1, + }, + ) return self.markdown.htmlStash.store(html) + article_list.meta = dict( - short_description=_('Article list'), - help_text=_('Insert a list of articles in this level.'), - example_code='[article_list depth:2]', - args={'depth': _('Maximum depth to show levels for.')} + short_description=_("Article list"), + help_text=_("Insert a list of articles in this level."), + example_code="[article_list depth:2]", + args={"depth": _("Maximum depth to show levels for.")}, ) def toc(self): return "[TOC]" + toc.meta = dict( - short_description=_('Table of contents'), - help_text=_('Insert a table of contents matching the headings.'), - example_code='[TOC]', - args={} + short_description=_("Table of contents"), + help_text=_("Insert a table of contents matching the headings."), + example_code="[TOC]", + args={}, ) def wikilink(self): return "" + wikilink.meta = dict( - short_description=_('WikiLinks'), - help_text=_( - 'Insert a link to another wiki page with a short notation.'), - example_code='[[WikiLink]]', - args={}) + short_description=_("WikiLinks"), + help_text=_("Insert a link to another wiki page with a short notation."), + example_code="[[WikiLink]]", + args={}, + ) def makeExtension(*args, **kwargs): diff --git a/src/wiki/plugins/macros/mdx/toc.py b/src/wiki/plugins/macros/mdx/toc.py index 5c520b27..981a6e4f 100644 --- a/src/wiki/plugins/macros/mdx/toc.py +++ b/src/wiki/plugins/macros/mdx/toc.py @@ -11,7 +11,6 @@ def wiki_slugify(*args, **kwargs): class WikiTreeProcessorClass(TocTreeprocessor): - def run(self, doc): # Necessary because self.title is set to a LazyObject via gettext_lazy if self.title: @@ -23,11 +22,11 @@ class WikiTocExtension(TocExtension): TreeProcessorClass = WikiTreeProcessorClass def __init__(self, **kwargs): - kwargs.setdefault('slugify', wiki_slugify) + kwargs.setdefault("slugify", wiki_slugify) super().__init__(**kwargs) def extendMarkdown(self, md): - if 'toc' in settings.METHODS: + if "toc" in settings.METHODS: TocExtension.extendMarkdown(self, md) diff --git a/src/wiki/plugins/macros/mdx/wikilinks.py b/src/wiki/plugins/macros/mdx/wikilinks.py index 08acbc5d..c3b8a8b8 100644 --- a/src/wiki/plugins/macros/mdx/wikilinks.py +++ b/src/wiki/plugins/macros/mdx/wikilinks.py @@ -10,27 +10,26 @@ from markdown.extensions import Extension, wikilinks def build_url(label, base, end, md): """ Build a url from the label, a base, and an end. """ - clean_label = re.sub(r'([ ]+_)|(_[ ]+)|([ ]+)', '_', label) + clean_label = re.sub(r"([ ]+_)|(_[ ]+)|([ ]+)", "_", label) urlpaths = md.article.urlpath_set.all() # Nevermind about the base we are fed, just keep the original # call pattern from the wikilinks plugin for later... - base = reverse('wiki:get', kwargs={'path': ''}) + base = reverse("wiki:get", kwargs={"path": ""}) for urlpath in urlpaths: if urlpath.children.filter(slug=clean_label).exists(): - base = '' + base = "" break - return '%s%s%s' % (base, clean_label, end) + return "%s%s%s" % (base, clean_label, end) class WikiLinkExtension(Extension): - def __init__(self, **kwargs): # set extension defaults self.config = { - 'base_url': ['', 'String to append to beginning or URL.'], - 'end_url': ['/', 'String to append to end of URL.'], - 'html_class': ['wiki_wikilink', 'CSS hook. Leave blank for none.'], - 'build_url': [build_url, 'Callable formats URL from label.'], + "base_url": ["", "String to append to beginning or URL."], + "end_url": ["/", "String to append to end of URL."], + "html_class": ["wiki_wikilink", "CSS hook. Leave blank for none."], + "build_url": [build_url, "Callable formats URL from label."], } super().__init__(**kwargs) @@ -38,23 +37,22 @@ class WikiLinkExtension(Extension): self.md = md # append to end of inline patterns - WIKILINK_RE = r'\[\[([\w0-9_ -]+)\]\]' + WIKILINK_RE = r"\[\[([\w0-9_ -]+)\]\]" wikilinkPattern = WikiLinks(WIKILINK_RE, self.getConfigs()) wikilinkPattern.md = md - md.inlinePatterns.add('wikilink', wikilinkPattern, "<not_strong") + md.inlinePatterns.add("wikilink", wikilinkPattern, "<not_strong") class WikiLinks(wikilinks.WikiLinksInlineProcessor): - def handleMatch(self, m, data): base_url, end_url, html_class = self._getMeta() label = m.group(1).strip() - url = self.config['build_url'](label, base_url, end_url, self.md) - a = markdown.util.etree.Element('a') + url = self.config["build_url"](label, base_url, end_url, self.md) + a = markdown.util.etree.Element("a") a.text = label - a.set('href', url) + a.set("href", url) if html_class: - a.set('class', html_class) + a.set("class", html_class) return a, m.start(0), m.end(0) diff --git a/src/wiki/plugins/macros/settings.py b/src/wiki/plugins/macros/settings.py index 5b316b11..c7042550 100644 --- a/src/wiki/plugins/macros/settings.py +++ b/src/wiki/plugins/macros/settings.py @@ -1,14 +1,9 @@ from django.conf import settings as django_settings -SLUG = 'macros' -APP_LABEL = 'wiki' +SLUG = "macros" +APP_LABEL = "wiki" #: List of markdown extensions this plugin should support. #: ``article_list`` inserts a list of articles from the current level. #: ``toc`` inserts a table of contents matching the headings. -METHODS = getattr( - django_settings, - 'WIKI_PLUGINS_METHODS', - ('article_list', - 'toc', - )) +METHODS = getattr(django_settings, "WIKI_PLUGINS_METHODS", ("article_list", "toc",)) diff --git a/src/wiki/plugins/macros/templatetags/wiki_macro_tags.py b/src/wiki/plugins/macros/templatetags/wiki_macro_tags.py index fb02e1fa..2ff2f146 100644 --- a/src/wiki/plugins/macros/templatetags/wiki_macro_tags.py +++ b/src/wiki/plugins/macros/templatetags/wiki_macro_tags.py @@ -6,12 +6,11 @@ register = template.Library() @register.inclusion_tag( - 'wiki/plugins/templatetags/article_list.html', - takes_context=True + "wiki/plugins/templatetags/article_list.html", takes_context=True ) def article_list(context, urlpath, depth): - context['parent'] = urlpath - context['depth'] = depth + context["parent"] = urlpath + context["depth"] = depth return context diff --git a/src/wiki/plugins/macros/wiki_plugin.py b/src/wiki/plugins/macros/wiki_plugin.py index 8e15d217..e1b601ca 100644 --- a/src/wiki/plugins/macros/wiki_plugin.py +++ b/src/wiki/plugins/macros/wiki_plugin.py @@ -8,16 +8,18 @@ class MacroPlugin(BasePlugin): slug = settings.SLUG - sidebar = {'headline': _('Macros'), - 'icon_class': 'fa-play', - 'template': 'wiki/plugins/macros/sidebar.html', - 'form_class': None, - 'get_form_kwargs': (lambda a: {})} + sidebar = { + "headline": _("Macros"), + "icon_class": "fa-play", + "template": "wiki/plugins/macros/sidebar.html", + "form_class": None, + "get_form_kwargs": (lambda a: {}), + } markdown_extensions = [ - 'wiki.plugins.macros.mdx.macro', - 'wiki.plugins.macros.mdx.toc', - 'wiki.plugins.macros.mdx.wikilinks', + "wiki.plugins.macros.mdx.macro", + "wiki.plugins.macros.mdx.toc", + "wiki.plugins.macros.mdx.wikilinks", ] diff --git a/src/wiki/plugins/notifications/__init__.py b/src/wiki/plugins/notifications/__init__.py index 67d70d98..1b5c544a 100644 --- a/src/wiki/plugins/notifications/__init__.py +++ b/src/wiki/plugins/notifications/__init__.py @@ -1 +1 @@ -default_app_config = 'wiki.plugins.notifications.apps.NotificationsConfig' +default_app_config = "wiki.plugins.notifications.apps.NotificationsConfig" diff --git a/src/wiki/plugins/notifications/apps.py b/src/wiki/plugins/notifications/apps.py index f4705ba5..acb4751a 100644 --- a/src/wiki/plugins/notifications/apps.py +++ b/src/wiki/plugins/notifications/apps.py @@ -4,9 +4,9 @@ from django.utils.translation import gettext_lazy as _ class NotificationsConfig(AppConfig): - name = 'wiki.plugins.notifications' + name = "wiki.plugins.notifications" verbose_name = _("Wiki notifications") - label = 'wiki_notifications' + label = "wiki_notifications" def ready(self): """ @@ -19,27 +19,31 @@ class NotificationsConfig(AppConfig): for plugin in registry.get_plugins(): - notifications = getattr(plugin, 'notifications', []) + notifications = getattr(plugin, "notifications", []) for notification_dict in notifications: + @disable_signal_for_loaddata def plugin_notification(instance, **kwargs): - if notification_dict.get('ignore', lambda x: False)(instance): + if notification_dict.get("ignore", lambda x: False)(instance): return - if kwargs.get('created', False) == notification_dict.get('created', True): - if 'get_url' in notification_dict: - url = notification_dict['get_url'](instance) + if kwargs.get("created", False) == notification_dict.get( + "created", True + ): + if "get_url" in notification_dict: + url = notification_dict["get_url"](instance) else: - url = models.default_url(notification_dict['get_article'](instance)) + url = models.default_url( + notification_dict["get_article"](instance) + ) - message = notification_dict['message'](instance) + message = notification_dict["message"](instance) notify( message, - notification_dict['key'], - target_object=notification_dict['get_article'](instance), + notification_dict["key"], + target_object=notification_dict["get_article"](instance), url=url, ) signals.post_save.connect( - plugin_notification, - sender=notification_dict['model'] + plugin_notification, sender=notification_dict["model"] ) diff --git a/src/wiki/plugins/notifications/forms.py b/src/wiki/plugins/notifications/forms.py index c34d9a8b..d866ed3c 100644 --- a/src/wiki/plugins/notifications/forms.py +++ b/src/wiki/plugins/notifications/forms.py @@ -10,50 +10,48 @@ from wiki.plugins.notifications.settings import ARTICLE_EDIT class SettingsModelChoiceField(forms.ModelChoiceField): - def label_from_instance(self, obj): - return gettext( - "Receive notifications %(interval)s" - ) % { - 'interval': obj.get_interval_display() + return gettext("Receive notifications %(interval)s") % { + "interval": obj.get_interval_display() } -class ArticleSubscriptionModelMultipleChoiceField( - forms.ModelMultipleChoiceField): - +class ArticleSubscriptionModelMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): return gettext("%(title)s - %(url)s") % { - 'title': obj.article.current_revision.title, - 'url': obj.article.get_absolute_url() + "title": obj.article.current_revision.title, + "url": obj.article.get_absolute_url(), } class SettingsModelForm(forms.ModelForm): - def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user') + self.user = kwargs.pop("user") super().__init__(*args, **kwargs) - instance = kwargs.get('instance', None) + instance = kwargs.get("instance", None) self.__editing_instance = False if instance: self.__editing_instance = True - self.fields['delete_subscriptions'] = ArticleSubscriptionModelMultipleChoiceField( + self.fields[ + "delete_subscriptions" + ] = ArticleSubscriptionModelMultipleChoiceField( models.ArticleSubscription.objects.filter( subscription__settings=instance, article__current_revision__deleted=False, ), label=gettext("Remove subscriptions"), required=False, - help_text=gettext("Select article subscriptions to remove from notifications"), + help_text=gettext( + "Select article subscriptions to remove from notifications" + ), initial=models.ArticleSubscription.objects.none(), ) - self.fields['email'] = forms.TypedChoiceField( + self.fields["email"] = forms.TypedChoiceField( label=_("Email digests"), choices=( - (0, gettext('Unchanged (selected on each article)')), - (1, gettext('No emails')), - (2, gettext('Email on any change')), + (0, gettext("Unchanged (selected on each article)")), + (1, gettext("No emails")), + (2, gettext("Email on any change")), ), coerce=lambda x: int(x) if x is not None else None, widget=forms.RadioSelect(), @@ -65,26 +63,21 @@ class SettingsModelForm(forms.ModelForm): instance = super().save(*args, commit=False, **kwargs) instance.user = self.user if self.__editing_instance: - self.cleaned_data['delete_subscriptions'].delete() - if self.cleaned_data['email'] == 1: - instance.subscription_set.all().update( - send_emails=False, - ) - elif self.cleaned_data['email'] == 2: - instance.subscription_set.all().update( - send_emails=True, - ) + self.cleaned_data["delete_subscriptions"].delete() + if self.cleaned_data["email"] == 1: + instance.subscription_set.all().update(send_emails=False,) + elif self.cleaned_data["email"] == 2: + instance.subscription_set.all().update(send_emails=True,) instance.save() return instance class BaseSettingsFormSet(BaseModelFormSet): - def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user') + self.user = kwargs.pop("user") # Ensure that at least 1 default settings object exists - all_settings = Settings.objects.filter(user=self.user).order_by('is_default') + all_settings = Settings.objects.filter(user=self.user).order_by("is_default") if not all_settings.exists(): Settings.objects.create(user=self.user, is_default=True) else: @@ -95,13 +88,15 @@ class BaseSettingsFormSet(BaseModelFormSet): super().__init__(*args, **kwargs) def get_queryset(self): - return Settings.objects.filter( - user=self.user, - ).exclude( - subscription__articlesubscription__article__current_revision__deleted=True, - ).prefetch_related( - 'subscription_set__articlesubscription', - ).order_by('is_default').distinct() + return ( + Settings.objects.filter(user=self.user,) + .exclude( + subscription__articlesubscription__article__current_revision__deleted=True, + ) + .prefetch_related("subscription_set__articlesubscription",) + .order_by("is_default") + .distinct() + ) SettingsFormSet = modelformset_factory( @@ -109,42 +104,36 @@ SettingsFormSet = modelformset_factory( form=SettingsModelForm, formset=BaseSettingsFormSet, extra=0, - fields=('interval', ), + fields=("interval",), ) class SubscriptionForm(PluginSettingsFormMixin, forms.Form): - settings_form_headline = _('Notifications') + settings_form_headline = _("Notifications") settings_order = 1 settings_write_access = False - settings = SettingsModelChoiceField( - None, - empty_label=None, - label=_('Settings') - ) - edit = forms.BooleanField( - required=False, - label=_('When this article is edited') - ) + settings = SettingsModelChoiceField(None, empty_label=None, label=_("Settings")) + edit = forms.BooleanField(required=False, label=_("When this article is edited")) edit_email = forms.BooleanField( - required=False, label=_('Also receive emails about article edits'), + required=False, + label=_("Also receive emails about article edits"), widget=forms.CheckboxInput( attrs={ - 'onclick': - mark_safe("$('#id_edit').attr('checked', $(this).is(':checked'));") + "onclick": mark_safe( + "$('#id_edit').attr('checked', $(this).is(':checked'));" + ) } - ) + ), ) def __init__(self, article, request, *args, **kwargs): self.article = article self.user = request.user - initial = kwargs.pop('initial', None) + initial = kwargs.pop("initial", None) self.notification_type = NotificationType.objects.get_or_create( - key=ARTICLE_EDIT, - content_type=ContentType.objects.get_for_model(article) + key=ARTICLE_EDIT, content_type=ContentType.objects.get_for_model(article) )[0] self.edit_notifications = models.ArticleSubscription.objects.filter( article=article, @@ -153,50 +142,49 @@ class SubscriptionForm(PluginSettingsFormMixin, forms.Form): ) self.default_settings = Settings.get_default_setting(request.user) if self.edit_notifications: - self.default_settings = self.edit_notifications[ - 0].subscription.settings + self.default_settings = self.edit_notifications[0].subscription.settings if not initial: initial = { - 'edit': bool(self.edit_notifications), - 'edit_email': bool(self.edit_notifications.filter(subscription__send_emails=True)), - 'settings': self.default_settings, + "edit": bool(self.edit_notifications), + "edit_email": bool( + self.edit_notifications.filter(subscription__send_emails=True) + ), + "settings": self.default_settings, } - kwargs['initial'] = initial + kwargs["initial"] = initial super().__init__(*args, **kwargs) - self.fields['settings'].queryset = Settings.objects.filter( - user=request.user, - ) + self.fields["settings"].queryset = Settings.objects.filter(user=request.user,) def get_usermessage(self): if self.changed_data: - return _('Your notification settings were updated.') + return _("Your notification settings were updated.") else: - return _( - 'Your notification settings were unchanged, so nothing saved.') + return _("Your notification settings were unchanged, so nothing saved.") def save(self, *args, **kwargs): if not self.changed_data: return - if self.cleaned_data['edit']: + if self.cleaned_data["edit"]: try: edit_notification = models.ArticleSubscription.objects.get( subscription__notification_type=self.notification_type, article=self.article, - subscription__settings=self.cleaned_data['settings'], + subscription__settings=self.cleaned_data["settings"], ) - edit_notification.subscription.send_emails = self.cleaned_data['edit_email'] + edit_notification.subscription.send_emails = self.cleaned_data[ + "edit_email" + ] edit_notification.subscription.save() except models.ArticleSubscription.DoesNotExist: subscription, __ = Subscription.objects.get_or_create( - settings=self.cleaned_data['settings'], + settings=self.cleaned_data["settings"], notification_type=self.notification_type, object_id=self.article.id, ) models.ArticleSubscription.objects.create( - subscription=subscription, - article=self.article, + subscription=subscription, article=self.article, ) - subscription.send_emails = self.cleaned_data['edit_email'] + subscription.send_emails = self.cleaned_data["edit_email"] subscription.save() else: diff --git a/src/wiki/plugins/notifications/management/commands/wiki_notifications_create_defaults.py b/src/wiki/plugins/notifications/management/commands/wiki_notifications_create_defaults.py index b4191892..a4454958 100644 --- a/src/wiki/plugins/notifications/management/commands/wiki_notifications_create_defaults.py +++ b/src/wiki/plugins/notifications/management/commands/wiki_notifications_create_defaults.py @@ -10,9 +10,9 @@ from wiki.plugins.notifications.settings import ARTICLE_EDIT class Command(BaseCommand): - args = '[file-name.csv]' + args = "[file-name.csv]" # @ReservedAssignment - help = 'Import and parse messages directly from a CSV file.' + help = "Import and parse messages directly from a CSV file." def handle(self, *args, **options): from django.conf import settings @@ -24,14 +24,14 @@ class Command(BaseCommand): def subscribe_to_article(article, user): if user not in settings_map: - settings_map[user], __ = Settings.objects.get_or_create( - user=user) + settings_map[user], __ = Settings.objects.get_or_create(user=user) return subscribe( settings_map[user], ARTICLE_EDIT, content_type=ContentType.objects.get_for_model(article), - object_id=article.id) + object_id=article.id, + ) subs = 0 articles = Article.objects.all() @@ -39,20 +39,25 @@ class Command(BaseCommand): if article.owner: subscription = subscribe_to_article(article, article.owner) models.ArticleSubscription.objects.get_or_create( - article=article, - subscription=subscription) + article=article, subscription=subscription + ) subs += 1 - for revision in article.articlerevision_set.exclude( - user=article.owner).exclude( - user=None).values('user').distinct(): - user = get_user_model().objects.get(id=revision['user']) + for revision in ( + article.articlerevision_set.exclude(user=article.owner) + .exclude(user=None) + .values("user") + .distinct() + ): + user = get_user_model().objects.get(id=revision["user"]) subs += 1 subscription = subscribe_to_article(article, user) models.ArticleSubscription.objects.get_or_create( - article=article, - subscription=subscription) - - self.stdout.write("Created {subs:d} subscriptions on {arts:d} articles".format( - subs=subs, - arts=articles.count(), - ), ending='\n') + article=article, subscription=subscription + ) + + self.stdout.write( + "Created {subs:d} subscriptions on {arts:d} articles".format( + subs=subs, arts=articles.count(), + ), + ending="\n", + ) diff --git a/src/wiki/plugins/notifications/migrations/0001_initial.py b/src/wiki/plugins/notifications/migrations/0001_initial.py index 059e9f7f..e43892fb 100644 --- a/src/wiki/plugins/notifications/migrations/0001_initial.py +++ b/src/wiki/plugins/notifications/migrations/0001_initial.py @@ -4,23 +4,37 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('django_nyt', '0006_auto_20141229_1630'), - ('wiki', '0001_initial'), + ("django_nyt", "0006_auto_20141229_1630"), + ("wiki", "0001_initial"), ] operations = [ migrations.CreateModel( - name='ArticleSubscription', + name="ArticleSubscription", fields=[ - ('articleplugin_ptr', models.OneToOneField(auto_created=True, to='wiki.ArticlePlugin', primary_key=True, parent_link=True, serialize=False, on_delete=models.CASCADE)), - ('subscription', models.OneToOneField(to='django_nyt.Subscription', on_delete=models.CASCADE)), + ( + "articleplugin_ptr", + models.OneToOneField( + auto_created=True, + to="wiki.ArticlePlugin", + primary_key=True, + parent_link=True, + serialize=False, + on_delete=models.CASCADE, + ), + ), + ( + "subscription", + models.OneToOneField( + to="django_nyt.Subscription", on_delete=models.CASCADE + ), + ), ], - options={ - }, - bases=('wiki.articleplugin',), + options={}, + bases=("wiki.articleplugin",), ), migrations.AlterUniqueTogether( - name='articlesubscription', - unique_together=set([('subscription', 'articleplugin_ptr')]), + name="articlesubscription", + unique_together=set([("subscription", "articleplugin_ptr")]), ), ] diff --git a/src/wiki/plugins/notifications/migrations/0002_auto_20151118_1811.py b/src/wiki/plugins/notifications/migrations/0002_auto_20151118_1811.py index d0e46a1c..c4b22b47 100644 --- a/src/wiki/plugins/notifications/migrations/0002_auto_20151118_1811.py +++ b/src/wiki/plugins/notifications/migrations/0002_auto_20151118_1811.py @@ -6,12 +6,11 @@ class Migration(migrations.Migration): atomic = False dependencies = [ - ('wiki_notifications', '0001_initial'), + ("wiki_notifications", "0001_initial"), ] operations = [ migrations.AlterModelTable( - name='articlesubscription', - table='wiki_notifications_articlesubscription', + name="articlesubscription", table="wiki_notifications_articlesubscription", ), ] diff --git a/src/wiki/plugins/notifications/models.py b/src/wiki/plugins/notifications/models.py index ac5590a6..4a5da3a7 100644 --- a/src/wiki/plugins/notifications/models.py +++ b/src/wiki/plugins/notifications/models.py @@ -16,61 +16,61 @@ class ArticleSubscription(ArticlePlugin): subscription = models.OneToOneField(Subscription, on_delete=models.CASCADE) def __str__(self): - title = (_("%(user)s subscribing to %(article)s (%(type)s)") % - {'user': self.subscription.settings.user.username, - 'article': self.article.current_revision.title, - 'type': self.subscription.notification_type.label}) + title = _("%(user)s subscribing to %(article)s (%(type)s)") % { + "user": self.subscription.settings.user.username, + "article": self.article.current_revision.title, + "type": self.subscription.notification_type.label, + } return str(title) class Meta: - unique_together = ('subscription', 'articleplugin_ptr') + unique_together = ("subscription", "articleplugin_ptr") # Matches label of upcoming 0.1 release - db_table = 'wiki_notifications_articlesubscription' + db_table = "wiki_notifications_articlesubscription" def default_url(article, urlpath=None): if urlpath: - return reverse('wiki:get', kwargs={'path': urlpath.path}) + return reverse("wiki:get", kwargs={"path": urlpath.path}) return article.get_absolute_url() @disable_signal_for_loaddata def post_article_revision_save(**kwargs): - instance = kwargs['instance'] - if kwargs.get('created', False): + instance = kwargs["instance"] + if kwargs.get("created", False): url = default_url(instance.article) - filter_exclude = {'settings__user': instance.user} + filter_exclude = {"settings__user": instance.user} if instance.deleted: notify( - _('Article deleted: %s') % - get_title(instance), + _("Article deleted: %s") % get_title(instance), settings.ARTICLE_EDIT, target_object=instance.article, url=url, - filter_exclude=filter_exclude) + filter_exclude=filter_exclude, + ) elif instance.previous_revision: notify( - _('Article modified: %s') % - get_title(instance), + _("Article modified: %s") % get_title(instance), settings.ARTICLE_EDIT, target_object=instance.article, url=url, - filter_exclude=filter_exclude) + filter_exclude=filter_exclude, + ) else: notify( - _('New article created: %s') % - get_title(instance), + _("New article created: %s") % get_title(instance), settings.ARTICLE_EDIT, target_object=instance, url=url, - filter_exclude=filter_exclude) + filter_exclude=filter_exclude, + ) # Whenever a new revision is created, we notifý users that an article # was edited signals.post_save.connect( - post_article_revision_save, - sender=wiki_models.ArticleRevision, + post_article_revision_save, sender=wiki_models.ArticleRevision, ) # TODO: We should notify users when the current_revision of an article is diff --git a/src/wiki/plugins/notifications/settings.py b/src/wiki/plugins/notifications/settings.py index 8a64b5e9..e36f5e50 100644 --- a/src/wiki/plugins/notifications/settings.py +++ b/src/wiki/plugins/notifications/settings.py @@ -1,8 +1,7 @@ - # Deprecated APP_LABEL = None # Key for django_nyt - changing it will break any existing notifications. ARTICLE_EDIT = "article_edit" -SLUG = 'notifications' +SLUG = "notifications" diff --git a/src/wiki/plugins/notifications/views.py b/src/wiki/plugins/notifications/views.py index 284df533..556e9043 100644 --- a/src/wiki/plugins/notifications/views.py +++ b/src/wiki/plugins/notifications/views.py @@ -10,7 +10,7 @@ from . import forms, models class NotificationSettings(FormView): - template_name = 'wiki/plugins/notifications/settings.html' + template_name = "wiki/plugins/notifications/settings.html" form_class = forms.SettingsFormSet @method_decorator(login_required) @@ -22,33 +22,39 @@ class NotificationSettings(FormView): settings = form.save() messages.info( self.request, - _("You will receive notifications %(interval)s for " - "%(articles)d articles") % { - 'interval': settings.get_interval_display(), - 'articles': self.get_article_subscriptions(form.instance).count(), - } + _( + "You will receive notifications %(interval)s for " + "%(articles)d articles" + ) + % { + "interval": settings.get_interval_display(), + "articles": self.get_article_subscriptions(form.instance).count(), + }, ) - return redirect('wiki:notification_settings') + return redirect("wiki:notification_settings") def get_article_subscriptions(self, nyt_settings): - return models.ArticleSubscription.objects.filter( - subscription__settings=nyt_settings, - article__current_revision__deleted=False, - ).select_related( - 'article', - 'article__current_revision' - ).distinct() + return ( + models.ArticleSubscription.objects.filter( + subscription__settings=nyt_settings, + article__current_revision__deleted=False, + ) + .select_related("article", "article__current_revision") + .distinct() + ) def get_form_kwargs(self): kwargs = super().get_form_kwargs() - kwargs['user'] = self.request.user - kwargs['form_kwargs'] = {'user': self.request.user} + kwargs["user"] = self.request.user + kwargs["form_kwargs"] = {"user": self.request.user} return kwargs def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['formset'] = context['form'] - for form in context['formset']: + context["formset"] = context["form"] + for form in context["formset"]: if form.instance: - form.instance.articlesubscriptions = self.get_article_subscriptions(form.instance) + form.instance.articlesubscriptions = self.get_article_subscriptions( + form.instance + ) return context diff --git a/src/wiki/plugins/notifications/wiki_plugin.py b/src/wiki/plugins/notifications/wiki_plugin.py index 7b20613a..4572b9cd 100644 --- a/src/wiki/plugins/notifications/wiki_plugin.py +++ b/src/wiki/plugins/notifications/wiki_plugin.py @@ -8,13 +8,19 @@ from . import settings, views class NotifyPlugin(BasePlugin): slug = settings.SLUG - urlpatterns = {'root': [ - re_path(r'^$', views.NotificationSettings.as_view(), name='notification_settings'), - ]} + urlpatterns = { + "root": [ + re_path( + r"^$", + views.NotificationSettings.as_view(), + name="notification_settings", + ), + ] + } article_view = views.NotificationSettings().dispatch - settings_form = 'wiki.plugins.notifications.forms.SubscriptionForm' + settings_form = "wiki.plugins.notifications.forms.SubscriptionForm" registry.register(NotifyPlugin) diff --git a/src/wiki/plugins/redlinks/__init__.py b/src/wiki/plugins/redlinks/__init__.py index f88b575f..80ac94d0 100644 --- a/src/wiki/plugins/redlinks/__init__.py +++ b/src/wiki/plugins/redlinks/__init__.py @@ -1 +1 @@ -default_app_config = 'wiki.plugins.redlinks.apps.RedlinksConfig' +default_app_config = "wiki.plugins.redlinks.apps.RedlinksConfig" diff --git a/src/wiki/plugins/redlinks/apps.py b/src/wiki/plugins/redlinks/apps.py index 5e8d6f5c..8a5e5007 100644 --- a/src/wiki/plugins/redlinks/apps.py +++ b/src/wiki/plugins/redlinks/apps.py @@ -3,6 +3,6 @@ from django.utils.translation import gettext_lazy as _ class RedlinksConfig(AppConfig): - name = 'wiki.plugins.redlinks' + name = "wiki.plugins.redlinks" verbose_name = _("Wiki red links") - label = 'wiki_redlinks' + label = "wiki_redlinks" diff --git a/src/wiki/plugins/redlinks/mdx/redlinks.py b/src/wiki/plugins/redlinks/mdx/redlinks.py index 0d068395..ba8cd525 100644 --- a/src/wiki/plugins/redlinks/mdx/redlinks.py +++ b/src/wiki/plugins/redlinks/mdx/redlinks.py @@ -25,7 +25,7 @@ def urljoin_internal(base, url): res1 = urljoin(canary1 + base, url) res2 = urljoin(canary2 + base, url) if res1.startswith(canary1) and res2.startswith(canary2): - return res1[len(canary1):] + return res1[len(canary1) :] class LinkTreeprocessor(Treeprocessor): diff --git a/src/wiki/plugins/redlinks/wiki_plugin.py b/src/wiki/plugins/redlinks/wiki_plugin.py index 5b1ecc1b..69ea17b9 100644 --- a/src/wiki/plugins/redlinks/wiki_plugin.py +++ b/src/wiki/plugins/redlinks/wiki_plugin.py @@ -5,7 +5,7 @@ from wiki.core.plugins.base import BasePlugin class Plugin(BasePlugin): markdown_extensions = [ - 'wiki.plugins.redlinks.mdx.redlinks', + "wiki.plugins.redlinks.mdx.redlinks", ] diff --git a/src/wiki/sites.py b/src/wiki/sites.py index 42dc1fd7..870bfd69 100644 --- a/src/wiki/sites.py +++ b/src/wiki/sites.py @@ -16,43 +16,77 @@ class WikiSite: of your customized site. """ - def __init__(self, name='wiki'): + def __init__(self, name="wiki"): from wiki.views import accounts, article, deleted_list self.name = name # root view self.root_view = getattr(self, "root_view", article.CreateRootView.as_view()) - self.root_missing_view = getattr(self, "root_missing_view", article.MissingRootView.as_view()) + self.root_missing_view = getattr( + self, "root_missing_view", article.MissingRootView.as_view() + ) # basic views self.article_view = getattr(self, "article_view", article.ArticleView.as_view()) - self.article_create_view = getattr(self, "article_create_view", article.Create.as_view()) - self.article_delete_view = getattr(self, "article_delete_view", article.Delete.as_view()) - self.article_deleted_view = getattr(self, "article_deleted_view", article.Deleted.as_view()) + self.article_create_view = getattr( + self, "article_create_view", article.Create.as_view() + ) + self.article_delete_view = getattr( + self, "article_delete_view", article.Delete.as_view() + ) + self.article_deleted_view = getattr( + self, "article_deleted_view", article.Deleted.as_view() + ) self.article_dir_view = getattr(self, "article_dir_view", article.Dir.as_view()) - self.article_edit_view = getattr(self, "article_edit_view", article.Edit.as_view()) - self.article_move_view = getattr(self, "article_move_view", article.Move.as_view()) - self.article_preview_view = getattr(self, "article_preview_view", article.Preview.as_view()) - self.article_history_view = getattr(self, "article_history_view", article.History.as_view()) - self.article_settings_view = getattr(self, "article_settings_view", article.Settings.as_view()) - self.article_source_view = getattr(self, "article_source_view", article.Source.as_view()) - self.article_plugin_view = getattr(self, "article_plugin_view", article.Plugin.as_view()) - self.revision_change_view = getattr(self, "revision_change_view", article.ChangeRevisionView.as_view()) - self.revision_merge_view = getattr(self, "revision_merge_view", article.MergeView.as_view()) - self.revision_preview_merge_view = getattr(self, "revision_preview_merge_view", article.MergeView.as_view(preview=True)) + self.article_edit_view = getattr( + self, "article_edit_view", article.Edit.as_view() + ) + self.article_move_view = getattr( + self, "article_move_view", article.Move.as_view() + ) + self.article_preview_view = getattr( + self, "article_preview_view", article.Preview.as_view() + ) + self.article_history_view = getattr( + self, "article_history_view", article.History.as_view() + ) + self.article_settings_view = getattr( + self, "article_settings_view", article.Settings.as_view() + ) + self.article_source_view = getattr( + self, "article_source_view", article.Source.as_view() + ) + self.article_plugin_view = getattr( + self, "article_plugin_view", article.Plugin.as_view() + ) + self.revision_change_view = getattr( + self, "revision_change_view", article.ChangeRevisionView.as_view() + ) + self.revision_merge_view = getattr( + self, "revision_merge_view", article.MergeView.as_view() + ) + self.revision_preview_merge_view = getattr( + self, "revision_preview_merge_view", article.MergeView.as_view(preview=True) + ) self.search_view = getattr(self, "search_view", article.SearchView.as_view()) - self.article_diff_view = getattr(self, "article_diff_view", article.DiffView.as_view()) + self.article_diff_view = getattr( + self, "article_diff_view", article.DiffView.as_view() + ) # account views self.signup_view = getattr(self, "signup_view", accounts.Signup.as_view()) self.login_view = getattr(self, "login_view", accounts.Login.as_view()) self.logout_view = getattr(self, "logout_view", accounts.Logout.as_view()) - self.profile_update_view = getattr(self, "profile_update_view", accounts.Update.as_view()) + self.profile_update_view = getattr( + self, "profile_update_view", accounts.Update.as_view() + ) # deleted list view - self.deleted_list_view = getattr(self, "deleted_list_view", deleted_list.DeletedListView.as_view()) + self.deleted_list_view = getattr( + self, "deleted_list_view", deleted_list.DeletedListView.as_view() + ) def get_urls(self): urlpatterns = self.get_root_urls() @@ -69,31 +103,39 @@ class WikiSite: @property def urls(self): - return self.get_urls(), 'wiki', self.name + return self.get_urls(), "wiki", self.name def get_root_urls(self): urlpatterns = [ - re_path(r'^$', self.article_view, name='root', kwargs={'path': ''}), - re_path(r'^create-root/$', self.root_view, name='root_create'), - re_path(r'^missing-root/$', self.root_missing_view, name='root_missing'), - re_path(r'^_search/$', self.search_view, name='search'), - re_path(r'^_revision/diff/(?P<revision_id>[0-9]+)/$', self.article_diff_view, name='diff'), + re_path(r"^$", self.article_view, name="root", kwargs={"path": ""}), + re_path(r"^create-root/$", self.root_view, name="root_create"), + re_path(r"^missing-root/$", self.root_missing_view, name="root_missing"), + re_path(r"^_search/$", self.search_view, name="search"), + re_path( + r"^_revision/diff/(?P<revision_id>[0-9]+)/$", + self.article_diff_view, + name="diff", + ), ] return urlpatterns def get_deleted_list_urls(self): urlpatterns = [ - re_path('^_admin/$', self.deleted_list_view, name="deleted_list"), + re_path("^_admin/$", self.deleted_list_view, name="deleted_list"), ] return urlpatterns def get_accounts_urls(self): if settings.ACCOUNT_HANDLING: urlpatterns = [ - re_path(r'^_accounts/sign-up/$', self.signup_view, name='signup'), - re_path(r'^_accounts/logout/$', self.logout_view, name='logout'), - re_path(r'^_accounts/login/$', self.login_view, name='login'), - re_path(r'^_accounts/settings/$', self.profile_update_view, name='profile_update'), + re_path(r"^_accounts/sign-up/$", self.signup_view, name="signup"), + re_path(r"^_accounts/logout/$", self.logout_view, name="logout"), + re_path(r"^_accounts/login/$", self.login_view, name="login"), + re_path( + r"^_accounts/settings/$", + self.profile_update_view, + name="profile_update", + ), ] else: urlpatterns = [] @@ -103,78 +145,128 @@ class WikiSite: urlpatterns = [ # This one doesn't work because it don't know # where to redirect after... - re_path(r'^change/(?P<revision_id>[0-9]+)/$', self.revision_change_view, name='change_revision'), - re_path(r'^preview/$', self.article_preview_view, name='preview_revision'), - re_path(r'^merge/(?P<revision_id>[0-9]+)/preview/$', self.revision_preview_merge_view, name='merge_revision_preview'), + re_path( + r"^change/(?P<revision_id>[0-9]+)/$", + self.revision_change_view, + name="change_revision", + ), + re_path(r"^preview/$", self.article_preview_view, name="preview_revision"), + re_path( + r"^merge/(?P<revision_id>[0-9]+)/preview/$", + self.revision_preview_merge_view, + name="merge_revision_preview", + ), ] return [ - re_path(r'^_revision/(?P<article_id>[0-9]+)/', include(urlpatterns)), + re_path(r"^_revision/(?P<article_id>[0-9]+)/", include(urlpatterns)), ] def get_article_urls(self): urlpatterns = [ # Paths decided by article_ids - re_path(r'^$', self.article_view, name='get'), - re_path(r'^delete/$', self.article_delete_view, name='delete'), - re_path(r'^deleted/$', self.article_deleted_view, name='deleted'), - re_path(r'^edit/$', self.article_edit_view, name='edit'), - re_path(r'^move/$', self.article_move_view, name='move'), - re_path(r'^preview/$', self.article_preview_view, name='preview'), - re_path(r'^history/$', self.article_history_view, name='history'), - re_path(r'^settings/$', self.article_settings_view, name='settings'), - re_path(r'^source/$', self.article_source_view, name='source'), - re_path(r'^revision/change/(?P<revision_id>[0-9]+)/$', self.revision_change_view, name='change_revision'), - re_path(r'^revision/merge/(?P<revision_id>[0-9]+)/$', self.revision_merge_view, name='merge_revision'), - re_path(r'^plugin/(?P<slug>\w+)/$', self.article_plugin_view, name='plugin'), + re_path(r"^$", self.article_view, name="get"), + re_path(r"^delete/$", self.article_delete_view, name="delete"), + re_path(r"^deleted/$", self.article_deleted_view, name="deleted"), + re_path(r"^edit/$", self.article_edit_view, name="edit"), + re_path(r"^move/$", self.article_move_view, name="move"), + re_path(r"^preview/$", self.article_preview_view, name="preview"), + re_path(r"^history/$", self.article_history_view, name="history"), + re_path(r"^settings/$", self.article_settings_view, name="settings"), + re_path(r"^source/$", self.article_source_view, name="source"), + re_path( + r"^revision/change/(?P<revision_id>[0-9]+)/$", + self.revision_change_view, + name="change_revision", + ), + re_path( + r"^revision/merge/(?P<revision_id>[0-9]+)/$", + self.revision_merge_view, + name="merge_revision", + ), + re_path( + r"^plugin/(?P<slug>\w+)/$", self.article_plugin_view, name="plugin" + ), ] return [ - re_path(r'^(?P<article_id>[0-9]+)/', include(urlpatterns)), + re_path(r"^(?P<article_id>[0-9]+)/", include(urlpatterns)), ] def get_article_path_urls(self): urlpatterns = [ # Paths decided by URLs - re_path(r'^(?P<path>.+/|)_create/$', self.article_create_view, name='create'), - re_path(r'^(?P<path>.+/|)_delete/$', self.article_delete_view, name='delete'), - re_path(r'^(?P<path>.+/|)_deleted/$', self.article_deleted_view, name='deleted'), - re_path(r'^(?P<path>.+/|)_edit/$', self.article_edit_view, name='edit'), - re_path(r'^(?P<path>.+/|)_move/$', self.article_move_view, name='move'), - re_path(r'^(?P<path>.+/|)_preview/$', self.article_preview_view, name='preview'), - re_path(r'^(?P<path>.+/|)_history/$', self.article_history_view, name='history'), - re_path(r'^(?P<path>.+/|)_dir/$', self.article_dir_view, name='dir'), - re_path(r'^(?P<path>.+/|)_search/$', self.search_view, name='search'), - re_path(r'^(?P<path>.+/|)_settings/$', self.article_settings_view, name='settings'), - re_path(r'^(?P<path>.+/|)_source/$', self.article_source_view, name='source'), - re_path(r'^(?P<path>.+/|)_revision/change/(?P<revision_id>[0-9]+)/$', self.revision_change_view, name='change_revision'), - re_path(r'^(?P<path>.+/|)_revision/merge/(?P<revision_id>[0-9]+)/$', self.revision_merge_view, name='merge_revision'), - re_path(r'^(?P<path>.+/|)_plugin/(?P<slug>\w+)/$', self.article_plugin_view, name='plugin'), + re_path( + r"^(?P<path>.+/|)_create/$", self.article_create_view, name="create" + ), + re_path( + r"^(?P<path>.+/|)_delete/$", self.article_delete_view, name="delete" + ), + re_path( + r"^(?P<path>.+/|)_deleted/$", self.article_deleted_view, name="deleted" + ), + re_path(r"^(?P<path>.+/|)_edit/$", self.article_edit_view, name="edit"), + re_path(r"^(?P<path>.+/|)_move/$", self.article_move_view, name="move"), + re_path( + r"^(?P<path>.+/|)_preview/$", self.article_preview_view, name="preview" + ), + re_path( + r"^(?P<path>.+/|)_history/$", self.article_history_view, name="history" + ), + re_path(r"^(?P<path>.+/|)_dir/$", self.article_dir_view, name="dir"), + re_path(r"^(?P<path>.+/|)_search/$", self.search_view, name="search"), + re_path( + r"^(?P<path>.+/|)_settings/$", + self.article_settings_view, + name="settings", + ), + re_path( + r"^(?P<path>.+/|)_source/$", self.article_source_view, name="source" + ), + re_path( + r"^(?P<path>.+/|)_revision/change/(?P<revision_id>[0-9]+)/$", + self.revision_change_view, + name="change_revision", + ), + re_path( + r"^(?P<path>.+/|)_revision/merge/(?P<revision_id>[0-9]+)/$", + self.revision_merge_view, + name="merge_revision", + ), + re_path( + r"^(?P<path>.+/|)_plugin/(?P<slug>\w+)/$", + self.article_plugin_view, + name="plugin", + ), # This should always go last! - re_path(r'^(?P<path>.+/|)$', self.article_view, name='get'), + re_path(r"^(?P<path>.+/|)$", self.article_view, name="get"), ] return urlpatterns def get_plugin_urls(self): urlpatterns = [] for plugin in registry.get_plugins().values(): - slug = getattr(plugin, 'slug', None) + slug = getattr(plugin, "slug", None) if slug: - article_urlpatterns = plugin.urlpatterns.get('article', []) + article_urlpatterns = plugin.urlpatterns.get("article", []) urlpatterns += [ - re_path(r'^(?P<article_id>[0-9]+)/plugin/' + slug + '/', - include(article_urlpatterns)), - re_path(r'^(?P<path>.+/|)_plugin/' + slug + '/', - include(article_urlpatterns)), + re_path( + r"^(?P<article_id>[0-9]+)/plugin/" + slug + "/", + include(article_urlpatterns), + ), + re_path( + r"^(?P<path>.+/|)_plugin/" + slug + "/", + include(article_urlpatterns), + ), ] - root_urlpatterns = plugin.urlpatterns.get('root', []) + root_urlpatterns = plugin.urlpatterns.get("root", []) urlpatterns += [ - re_path(r'^_plugin/' + slug + '/', include(root_urlpatterns)), + re_path(r"^_plugin/" + slug + "/", include(root_urlpatterns)), ] return urlpatterns class DefaultWikiSite(LazyObject): def _setup(self): - WikiSiteClass = import_string(apps.get_app_config('wiki').default_site) + WikiSiteClass = import_string(apps.get_app_config("wiki").default_site) self._wrapped = WikiSiteClass() diff --git a/src/wiki/static/wiki/bootstrap/css/wiki-bootstrap.min.css b/src/wiki/static/wiki/bootstrap/css/wiki-bootstrap.min.css index a1408982..50d0a690 100644 --- a/src/wiki/static/wiki/bootstrap/css/wiki-bootstrap.min.css +++ b/src/wiki/static/wiki/bootstrap/css/wiki-bootstrap.min.css @@ -10,4 +10,4 @@ *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,*:before,*:after{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{background-color:#fcf8e3;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover,a.text-primary:focus{color:#286090}.text-success{color:#3c763d}a.text-success:hover,a.text-success:focus{color:#2b542c}.text-info{color:#31708f}a.text-info:hover,a.text-info:focus{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover,a.text-warning:focus{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover,a.text-danger:focus{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover,a.bg-primary:focus{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover,a.bg-success:focus{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover,a.bg-info:focus{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover,a.bg-warning:focus{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover,a.bg-danger:focus{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:bold;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:34px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:30px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-left:-20px;margin-top:4px \9}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}@media (min-width:768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:focus,.btn-default.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#fff;background-color:#398439;border-color:#255625}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#337ab7;font-weight:normal;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height, visibility;transition-property:height, visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#337ab7}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-right-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}}@media (min-width:768px){.navbar-left{float:left !important;float:left}.navbar-right{float:right !important;float:right;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#337ab7;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:3;color:#fff;background-color:#337ab7;border-color:#337ab7;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;color:#fff;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px;padding-left:15px;padding-right:15px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{text-decoration:none;color:#555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{background-color:#eee;color:#777;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-left:15px;padding-right:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:hidden;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);-o-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform 0.3s ease-out;-moz-transition:-moz-transform 0.3s ease-out;-o-transition:-o-transform 0.3s ease-out;transition:transform 0.3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:12px;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;right:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:14px;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,0.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,0.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform 0.6s ease-in-out;-moz-transition:-moz-transform 0.6s ease-in-out;-o-transition:-o-transform 0.6s ease-in-out;transition:transform 0.6s ease-in-out;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;-moz-perspective:1000px;perspective:1000px}.carousel-inner>.item.next,.carousel-inner>.item.active.right{-webkit-transform:translate3d(100%, 0, 0);transform:translate3d(100%, 0, 0);left:0}.carousel-inner>.item.prev,.carousel-inner>.item.active.left{-webkit-transform:translate3d(-100%, 0, 0);transform:translate3d(-100%, 0, 0);left:0}.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right,.carousel-inner>.item.active{-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);background-color:rgba(0,0,0,0)}.carousel-control.left{background-image:-webkit-linear-gradient(left, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-image:-o-linear-gradient(left, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-image:linear-gradient(to right, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-image:-o-linear-gradient(left, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-image:linear-gradient(to right, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;margin-top:-10px;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;line-height:1;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-header:before,.modal-header:after,.modal-footer:before,.modal-footer:after,.form-actions:before,.form-actions:after{content:" ";display:table}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-header:after,.modal-footer:after,.form-actions:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1200px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1200px){.visible-lg-block{display:block !important}}@media (min-width:1200px){.visible-lg-inline{display:inline !important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media (min-width:1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}.wiki-article .thumbnail{clear:both;margin-bottom:15px;margin-left:10px;margin-right:10px}#div_id_title .asteriskField{display:none}#id_title{font-size:20px;height:40px;padding:6px;display:block;width:98%}#id_summary{width:98%;padding:6px}#id_content{width:100%;padding:6px}h1#article-title{font-size:2.5em;margin-top:0}.article-edit-title-link{font-size:14px;padding-left:8px}.wiki-label label{font-size:16px;font-weight:normal;color:#777}.controls ul{margin-left:0;list-style:none}#attachment_form #id_description{width:95%}.wiki-article div.toc,.wiki-article div.article-list{max-width:340px;clear:left;display:inline-block}.wiki-article div.toc .nav-header,.wiki-article div.article-list .nav-header{padding:3px 10px;border-bottom:1px solid #ddd;font-weight:bold}.wiki-article div.toc ul,.wiki-article div.article-list ul{padding-left:0;list-style:none}.wiki-article div.toc>ul,.wiki-article div.article-list>ul{border-radius:5px;background-color:#f6f6f6;padding-top:5px}.wiki-article div.toc ul li ul li,.wiki-article div.article-list ul li ul li{padding-left:10px}.wiki-article div.toc ul li a,.wiki-article div.article-list ul li a{padding:5px 10px;display:block;border-bottom:1px solid #ddd}.wiki-article div.toc ul li:last-child,.wiki-article div.article-list ul li:last-child{margin-bottom:7px}.wiki-article div.toc .toctitle{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.wiki-article a.linknotfound{color:#c87}.wiki-broken{color:#ba0000}.wiki-article pre{max-width:700px;max-height:150px;overflow:auto;word-wrap:normal;white-space:pre}.wiki-article .codehilitetable{max-width:700px;max-height:150px;background-color:#f5f5f5;border-color:#ccc;border-radius:4px;border-collapse:separate;display:block;overflow:auto}.wiki-article .codehilitetable td:first-child{border-left:none;border-top:none;border-bottom:none}.wiki-article .codehilitetable td:last-child{border:none}.wiki-article .codehilitetable td{padding:0}.wiki-article .codehilitetable pre{margin-bottom:0}.wiki-article table{width:100%;max-width:100%;margin-bottom:20px;border:1px solid #ddd}.wiki-article table>thead>tr>th,.wiki-article table>tbody>tr>th,.wiki-article table>tfoot>tr>th,.wiki-article table>thead>tr>td,.wiki-article table>tbody>tr>td,.wiki-article table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.wiki-article table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.wiki-article table>caption+thead>tr:first-child>th,.wiki-article table>colgroup+thead>tr:first-child>th,.wiki-article table>thead:first-child>tr:first-child>th,.wiki-article table>caption+thead>tr:first-child>td,.wiki-article table>colgroup+thead>tr:first-child>td,.wiki-article table>thead:first-child>tr:first-child>td{border-top:0}.wiki-article table>tbody+tbody{border-top:2px solid #ddd}.wiki-article table .table{background-color:#fff}.wiki-article table>thead>tr>th,.wiki-article table>tbody>tr>th,.wiki-article table>tfoot>tr>th,.wiki-article table>thead>tr>td,.wiki-article table>tbody>tr>td,.wiki-article table>tfoot>tr>td{border:1px solid #ddd}.wiki-article table>thead>tr>th,.wiki-article table>thead>tr>td{border-bottom-width:2px}.table-responsive{border:none}.wiki-article h1,.wiki-article h2,.wiki-article h3,.wiki-article h4,.wiki-article h5,.wiki-article h6{overflow:hidden;margin-right:5px}.wiki-article h1{font-size:28px;padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee;clear:both}.wiki-article h2{font-size:24.5px;padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee;margin-bottom:20px;padding-bottom:0}.wiki-article h3{font-size:21px;margin:15px 0 10px;line-height:30px}.wiki-article h4{font-size:17.5px}.wiki-article h5,.wiki-article h6{font-size:14px}.wiki-article blockquote p,.wiki-article blockquote{font-size:14px}input[type=file]{float:none;width:auto}.asteriskField{font-size:20px;margin-left:5px}.notification-li .since{font-size:80%;color:#ccc}.directory-toolbar .filter-clear{margin-right:10px;position:relative;top:5px}.panel-heading h3{margin:0}.diff-container{overflow-x:scroll}.breadcrumb .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;border-radius:1px 1px 1px 1px;box-shadow:0 1px 0 rgba(0,0,0,0.25);margin-top:3px}.breadcrumb .icon-bar:first-child{margin-top:0}#article-menu{border-bottom:1px solid #eee;padding-bottom:0;margin-bottom:20px}#article-container{margin-top:20px}#wiki-footer{padding:30px 0;clear:both}.wiki-modal .modal-body iframe{width:100%;min-height:400px;height:100%;border:0}.ui-resizable-s{bottom:0}.ui-resizable-e{right:0}.ui-resizable{position:fixed !important}@media print{.navbar,.nav-tabs li a,#article-breadcrumbs{display:none}#article-menu li{display:none}#article-title-li{display:block !important}}.wiki-form-block input[type='text'],.wiki-form-block select{width:auto;display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s}.wiki-form-block input[type='text']:focus,.wiki-form-block select:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6)}.wiki-form-block input[type='text']::-moz-placeholder,.wiki-form-block select::-moz-placeholder{color:#999;opacity:1}.wiki-form-block input[type='text']:-ms-input-placeholder,.wiki-form-block select:-ms-input-placeholder{color:#999}.wiki-form-block input[type='text']::-webkit-input-placeholder,.wiki-form-block select::-webkit-input-placeholder{color:#999}.wiki-form-block input[type='text']::-ms-expand,.wiki-form-block select::-ms-expand{border:0;background-color:transparent}.wiki-form-block input[type='text'][disabled],.wiki-form-block select[disabled],.wiki-form-block input[type='text'][readonly],.wiki-form-block select[readonly],fieldset[disabled] .wiki-form-block input[type='text'],fieldset[disabled] .wiki-form-block select{background-color:#eee;opacity:1}.wiki-form-block input[type='text'][disabled],.wiki-form-block select[disabled],fieldset[disabled] .wiki-form-block input[type='text'],fieldset[disabled] .wiki-form-block select{cursor:not-allowed}textarea.wiki-form-block input[type='text'],textarea.wiki-form-block select{height:auto}.wiki-control input[type=text],.wiki-control input[type=email],.wiki-control input[type=password]{height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;vertical-align:middle;width:280px;display:inline-block}.form-horizontal .wiki-control input[type='text'],.form-horizontal .wiki-control input[type='password'],.form-horizontal .wiki-control input[type='email'],.form-horizontal .wiki-control select,.form-horizontal .wiki-control textarea{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;width:280px;display:inline-block}.form-horizontal .wiki-control input[type='text']:focus,.form-horizontal .wiki-control input[type='password']:focus,.form-horizontal .wiki-control input[type='email']:focus,.form-horizontal .wiki-control select:focus,.form-horizontal .wiki-control textarea:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6)}.form-horizontal .wiki-control input[type='text']::-moz-placeholder,.form-horizontal .wiki-control input[type='password']::-moz-placeholder,.form-horizontal .wiki-control input[type='email']::-moz-placeholder,.form-horizontal .wiki-control select::-moz-placeholder,.form-horizontal .wiki-control textarea::-moz-placeholder{color:#999;opacity:1}.form-horizontal .wiki-control input[type='text']:-ms-input-placeholder,.form-horizontal .wiki-control input[type='password']:-ms-input-placeholder,.form-horizontal .wiki-control input[type='email']:-ms-input-placeholder,.form-horizontal .wiki-control select:-ms-input-placeholder,.form-horizontal .wiki-control textarea:-ms-input-placeholder{color:#999}.form-horizontal .wiki-control input[type='text']::-webkit-input-placeholder,.form-horizontal .wiki-control input[type='password']::-webkit-input-placeholder,.form-horizontal .wiki-control input[type='email']::-webkit-input-placeholder,.form-horizontal .wiki-control select::-webkit-input-placeholder,.form-horizontal .wiki-control textarea::-webkit-input-placeholder{color:#999}.form-horizontal .wiki-control input[type='text']::-ms-expand,.form-horizontal .wiki-control input[type='password']::-ms-expand,.form-horizontal .wiki-control input[type='email']::-ms-expand,.form-horizontal .wiki-control select::-ms-expand,.form-horizontal .wiki-control textarea::-ms-expand{border:0;background-color:transparent}.form-horizontal .wiki-control input[type='text'][disabled],.form-horizontal .wiki-control input[type='password'][disabled],.form-horizontal .wiki-control input[type='email'][disabled],.form-horizontal .wiki-control select[disabled],.form-horizontal .wiki-control textarea[disabled],.form-horizontal .wiki-control input[type='text'][readonly],.form-horizontal .wiki-control input[type='password'][readonly],.form-horizontal .wiki-control input[type='email'][readonly],.form-horizontal .wiki-control select[readonly],.form-horizontal .wiki-control textarea[readonly],fieldset[disabled] .form-horizontal .wiki-control input[type='text'],fieldset[disabled] .form-horizontal .wiki-control input[type='password'],fieldset[disabled] .form-horizontal .wiki-control input[type='email'],fieldset[disabled] .form-horizontal .wiki-control select,fieldset[disabled] .form-horizontal .wiki-control textarea{background-color:#eee;opacity:1}.form-horizontal .wiki-control input[type='text'][disabled],.form-horizontal .wiki-control input[type='password'][disabled],.form-horizontal .wiki-control input[type='email'][disabled],.form-horizontal .wiki-control select[disabled],.form-horizontal .wiki-control textarea[disabled],fieldset[disabled] .form-horizontal .wiki-control input[type='text'],fieldset[disabled] .form-horizontal .wiki-control input[type='password'],fieldset[disabled] .form-horizontal .wiki-control input[type='email'],fieldset[disabled] .form-horizontal .wiki-control select,fieldset[disabled] .form-horizontal .wiki-control textarea{cursor:not-allowed}textarea.form-horizontal .wiki-control input[type='text'],textarea.form-horizontal .wiki-control input[type='password'],textarea.form-horizontal .wiki-control input[type='email'],textarea.form-horizontal .wiki-control select,textarea.form-horizontal .wiki-control textarea{height:auto}.form-horizontal .wiki-control input[type=text],.form-horizontal .wiki-control input[type=password]{width:280px;display:inline-block}.form-horizontal .wiki-control .input-group{width:280px;display:inline-table}.form-vertical .wiki-control textarea,.form-horizontal .wiki-control textarea{height:200px}.form-vertical .wiki-control input[type='text'],.form-vertical .wiki-control select,.form-vertical .wiki-control textarea{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;width:95%;display:inline-block}.form-vertical .wiki-control input[type='text']:focus,.form-vertical .wiki-control select:focus,.form-vertical .wiki-control textarea:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6)}.form-vertical .wiki-control input[type='text']::-moz-placeholder,.form-vertical .wiki-control select::-moz-placeholder,.form-vertical .wiki-control textarea::-moz-placeholder{color:#999;opacity:1}.form-vertical .wiki-control input[type='text']:-ms-input-placeholder,.form-vertical .wiki-control select:-ms-input-placeholder,.form-vertical .wiki-control textarea:-ms-input-placeholder{color:#999}.form-vertical .wiki-control input[type='text']::-webkit-input-placeholder,.form-vertical .wiki-control select::-webkit-input-placeholder,.form-vertical .wiki-control textarea::-webkit-input-placeholder{color:#999}.form-vertical .wiki-control input[type='text']::-ms-expand,.form-vertical .wiki-control select::-ms-expand,.form-vertical .wiki-control textarea::-ms-expand{border:0;background-color:transparent}.form-vertical .wiki-control input[type='text'][disabled],.form-vertical .wiki-control select[disabled],.form-vertical .wiki-control textarea[disabled],.form-vertical .wiki-control input[type='text'][readonly],.form-vertical .wiki-control select[readonly],.form-vertical .wiki-control textarea[readonly],fieldset[disabled] .form-vertical .wiki-control input[type='text'],fieldset[disabled] .form-vertical .wiki-control select,fieldset[disabled] .form-vertical .wiki-control textarea{background-color:#eee;opacity:1}.form-vertical .wiki-control input[type='text'][disabled],.form-vertical .wiki-control select[disabled],.form-vertical .wiki-control textarea[disabled],fieldset[disabled] .form-vertical .wiki-control input[type='text'],fieldset[disabled] .form-vertical .wiki-control select,fieldset[disabled] .form-vertical .wiki-control textarea{cursor:not-allowed}textarea.form-vertical .wiki-control input[type='text'],textarea.form-vertical .wiki-control select,textarea.form-vertical .wiki-control textarea{height:auto}.form-vertical .wiki-control,.form-vertical .wiki-label{display:block;float:none;padding:0;margin:0;width:100%}.form-actions{margin-left:-15px;margin-right:-15px;padding:19px 15px 20px;padding-left:0;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;float:none}.panel-heading .icon{color:#444;font-size:75%}.wiki-control select[multiple],.wiki-control select[size]{height:auto}.wiki-modal .modal-content{width:100%}@media (min-width:980px){.wiki-modal .modal-dialog{width:80%}}@media (max-width:979px){.wiki-modal .modal-dialog{width:90%}}@media (max-width:767px){.wiki-modal .modal-dialog{width:90%}}@media screen and (min-width:768px){.pull-right-block-on-responsive{float:right !important;float:right}#wiki-search-form{width:170px}}/*! * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('../../font-awesome/font/fontawesome-webfont.eot?v=4.2.0');src:url('../../font-awesome/font/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'),url('../../font-awesome/font/fontawesome-webfont.woff?v=4.2.0') format('woff'),url('../../font-awesome/font/fontawesome-webfont.ttf?v=4.2.0') format('truetype'),url('../../font-awesome/font/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.wiki-article .codehilite-wrap{overflow:auto;max-width:auto;max-height:250px}.wiki-article .codehilitetable pre{max-width:none;max-height:none;border:0;background-color:transparent}.wiki-article .codehilitetable .linenos{width:1%;white-space:nowrap}.codehilite .hll{background-color:#ffc}.codehilite .c{color:#808080}.codehilite .err{color:#f00000;background-color:#f0a0a0}.codehilite .k{color:#008000;font-weight:bold}.codehilite .o{color:#303030}.codehilite .cm{color:#808080}.codehilite .cp{color:#507090}.codehilite .c1{color:#808080}.codehilite .cs{color:#c00;font-weight:bold}.codehilite .gd{color:#a00000}.codehilite .ge{font-style:italic}.codehilite .gr{color:#f00}.codehilite .gh{color:#000080;font-weight:bold}.codehilite .gi{color:#00a000}.codehilite .go{color:#808080}.codehilite .gp{color:#c65d09;font-weight:bold}.codehilite .gs{font-weight:bold}.codehilite .gu{color:#800080;font-weight:bold}.codehilite .gt{color:#0040d0}.codehilite .kc{color:#008000;font-weight:bold}.codehilite .kd{color:#008000;font-weight:bold}.codehilite .kn{color:#008000;font-weight:bold}.codehilite .kp{color:#003080;font-weight:bold}.codehilite .kr{color:#008000;font-weight:bold}.codehilite .kt{color:#303090;font-weight:bold}.codehilite .m{color:#6000e0;font-weight:bold}.codehilite .s{background-color:#fff0f0}.codehilite .na{color:#0000c0}.codehilite .nb{color:#007020}.codehilite .nc{color:#b00060;font-weight:bold}.codehilite .no{color:#003060;font-weight:bold}.codehilite .nd{color:#505050;font-weight:bold}.codehilite .ni{color:#800000;font-weight:bold}.codehilite .ne{color:#f00000;font-weight:bold}.codehilite .nf{color:#0060b0;font-weight:bold}.codehilite .nl{color:#907000;font-weight:bold}.codehilite .nn{color:#0e84b5;font-weight:bold}.codehilite .nt{color:#007000}.codehilite .nv{color:#906030}.codehilite .ow{color:#000;font-weight:bold}.codehilite .w{color:#bbb}.codehilite .mf{color:#6000e0;font-weight:bold}.codehilite .mh{color:#005080;font-weight:bold}.codehilite .mi{color:#0000d0;font-weight:bold}.codehilite .mo{color:#4000e0;font-weight:bold}.codehilite .sb{background-color:#fff0f0}.codehilite .sc{color:#0040d0}.codehilite .sd{color:#d04020}.codehilite .s2{background-color:#fff0f0}.codehilite .se{color:#606060;font-weight:bold;background-color:#fff0f0}.codehilite .sh{background-color:#fff0f0}.codehilite .si{background-color:#e0e0e0}.codehilite .sx{color:#d02000;background-color:#fff0f0}.codehilite .sr{color:#000;background-color:#fff0ff}.codehilite .s1{background-color:#fff0f0}.codehilite .ss{color:#a06000}.codehilite .bp{color:#007020}.codehilite .vc{color:#306090}.codehilite .vg{color:#d07000;font-weight:bold}.codehilite .vi{color:#3030b0}.codehilite .il{color:#0000d0;font-weight:bold}.twitter-typeahead{width:100%;position:relative}.twitter-typeahead .tt-query,.twitter-typeahead .tt-hint{margin-bottom:0;width:100%;position:absolute;top:0;left:0}.twitter-typeahead .tt-hint{color:#a1a1a1;z-index:1;padding:6px 12px;border:1px solid transparent}.twitter-typeahead .tt-query{z-index:2;border-radius:4px !important;border-top-right-radius:0 !important;border-bottom-right-radius:0 !important}.tt-dropdown-menu{min-width:160px;margin-top:2px;padding:5px 0;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.tt-suggestion{display:block;padding:3px 20px}.tt-suggestion.tt-is-under-cursor{color:#fff;background-color:#0081c2;background-image:-moz-linear-gradient(top, #08c, #0077b3);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#08c), to(#0077b3));background-image:-webkit-linear-gradient(top, #08c, #0077b3);background-image:-o-linear-gradient(top, #08c, #0077b3);background-image:linear-gradient(to bottom, #08c, #0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0)}.tt-suggestion.tt-is-under-cursor a{color:#fff}.tt-suggestion p{margin:0}.twitter-typeahead .tt-hint{display:block;height:38px;padding:8px 12px;font-size:14px;line-height:1.42857143;border:1px solid transparent}.twitter-typeahead{display:block !important}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropdown-submenu>a:after{display:block;content:" ";float:right;width:0;height:0;border-color:transparent;border-style:solid;border-width:5px 0 5px 5px;border-left-color:#ccc;margin-top:5px;margin-right:-10px}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px} \ No newline at end of file + */@font-face{font-family:'FontAwesome';src:url('../../font-awesome/font/fontawesome-webfont.eot?v=4.2.0');src:url('../../font-awesome/font/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'),url('../../font-awesome/font/fontawesome-webfont.woff?v=4.2.0') format('woff'),url('../../font-awesome/font/fontawesome-webfont.ttf?v=4.2.0') format('truetype'),url('../../font-awesome/font/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.wiki-article .codehilite-wrap{overflow:auto;max-width:auto;max-height:250px}.wiki-article .codehilitetable pre{max-width:none;max-height:none;border:0;background-color:transparent}.wiki-article .codehilitetable .linenos{width:1%;white-space:nowrap}.codehilite .hll{background-color:#ffc}.codehilite .c{color:#808080}.codehilite .err{color:#f00000;background-color:#f0a0a0}.codehilite .k{color:#008000;font-weight:bold}.codehilite .o{color:#303030}.codehilite .cm{color:#808080}.codehilite .cp{color:#507090}.codehilite .c1{color:#808080}.codehilite .cs{color:#c00;font-weight:bold}.codehilite .gd{color:#a00000}.codehilite .ge{font-style:italic}.codehilite .gr{color:#f00}.codehilite .gh{color:#000080;font-weight:bold}.codehilite .gi{color:#00a000}.codehilite .go{color:#808080}.codehilite .gp{color:#c65d09;font-weight:bold}.codehilite .gs{font-weight:bold}.codehilite .gu{color:#800080;font-weight:bold}.codehilite .gt{color:#0040d0}.codehilite .kc{color:#008000;font-weight:bold}.codehilite .kd{color:#008000;font-weight:bold}.codehilite .kn{color:#008000;font-weight:bold}.codehilite .kp{color:#003080;font-weight:bold}.codehilite .kr{color:#008000;font-weight:bold}.codehilite .kt{color:#303090;font-weight:bold}.codehilite .m{color:#6000e0;font-weight:bold}.codehilite .s{background-color:#fff0f0}.codehilite .na{color:#0000c0}.codehilite .nb{color:#007020}.codehilite .nc{color:#b00060;font-weight:bold}.codehilite .no{color:#003060;font-weight:bold}.codehilite .nd{color:#505050;font-weight:bold}.codehilite .ni{color:#800000;font-weight:bold}.codehilite .ne{color:#f00000;font-weight:bold}.codehilite .nf{color:#0060b0;font-weight:bold}.codehilite .nl{color:#907000;font-weight:bold}.codehilite .nn{color:#0e84b5;font-weight:bold}.codehilite .nt{color:#007000}.codehilite .nv{color:#906030}.codehilite .ow{color:#000;font-weight:bold}.codehilite .w{color:#bbb}.codehilite .mf{color:#6000e0;font-weight:bold}.codehilite .mh{color:#005080;font-weight:bold}.codehilite .mi{color:#0000d0;font-weight:bold}.codehilite .mo{color:#4000e0;font-weight:bold}.codehilite .sb{background-color:#fff0f0}.codehilite .sc{color:#0040d0}.codehilite .sd{color:#d04020}.codehilite .s2{background-color:#fff0f0}.codehilite .se{color:#606060;font-weight:bold;background-color:#fff0f0}.codehilite .sh{background-color:#fff0f0}.codehilite .si{background-color:#e0e0e0}.codehilite .sx{color:#d02000;background-color:#fff0f0}.codehilite .sr{color:#000;background-color:#fff0ff}.codehilite .s1{background-color:#fff0f0}.codehilite .ss{color:#a06000}.codehilite .bp{color:#007020}.codehilite .vc{color:#306090}.codehilite .vg{color:#d07000;font-weight:bold}.codehilite .vi{color:#3030b0}.codehilite .il{color:#0000d0;font-weight:bold}.twitter-typeahead{width:100%;position:relative}.twitter-typeahead .tt-query,.twitter-typeahead .tt-hint{margin-bottom:0;width:100%;position:absolute;top:0;left:0}.twitter-typeahead .tt-hint{color:#a1a1a1;z-index:1;padding:6px 12px;border:1px solid transparent}.twitter-typeahead .tt-query{z-index:2;border-radius:4px !important;border-top-right-radius:0 !important;border-bottom-right-radius:0 !important}.tt-dropdown-menu{min-width:160px;margin-top:2px;padding:5px 0;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.tt-suggestion{display:block;padding:3px 20px}.tt-suggestion.tt-is-under-cursor{color:#fff;background-color:#0081c2;background-image:-moz-linear-gradient(top, #08c, #0077b3);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#08c), to(#0077b3));background-image:-webkit-linear-gradient(top, #08c, #0077b3);background-image:-o-linear-gradient(top, #08c, #0077b3);background-image:linear-gradient(to bottom, #08c, #0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0)}.tt-suggestion.tt-is-under-cursor a{color:#fff}.tt-suggestion p{margin:0}.twitter-typeahead .tt-hint{display:block;height:38px;padding:8px 12px;font-size:14px;line-height:1.42857143;border:1px solid transparent}.twitter-typeahead{display:block !important}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropdown-submenu>a:after{display:block;content:" ";float:right;width:0;height:0;border-color:transparent;border-style:solid;border-width:5px 0 5px 5px;border-left-color:#ccc;margin-top:5px;margin-right:-10px}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px} diff --git a/src/wiki/templatetags/wiki_tags.py b/src/wiki/templatetags/wiki_tags.py index 94030364..1e23a377 100644 --- a/src/wiki/templatetags/wiki_tags.py +++ b/src/wiki/templatetags/wiki_tags.py @@ -36,54 +36,54 @@ def article_for_object(context, obj): if True or obj not in _cache: try: article = models.ArticleForObject.objects.get( - content_type=content_type, - object_id=obj.pk).article + content_type=content_type, object_id=obj.pk + ).article except models.ArticleForObject.DoesNotExist: article = None _cache[obj] = article return _cache[obj] -@register.inclusion_tag('wiki/includes/render.html', takes_context=True) +@register.inclusion_tag("wiki/includes/render.html", takes_context=True) def wiki_render(context, article, preview_content=None): if preview_content: content = article.render(preview_content=preview_content) elif article.current_revision: - content = article.get_cached_content(user=context.get('user')) + content = article.get_cached_content(user=context.get("user")) else: content = None - context.update({ - 'article': article, - 'content': content, - 'preview': preview_content is not None, - 'plugins': plugin_registry.get_plugins(), - 'STATIC_URL': django_settings.STATIC_URL, - 'CACHE_TIMEOUT': settings.CACHE_TIMEOUT, - }) + context.update( + { + "article": article, + "content": content, + "preview": preview_content is not None, + "plugins": plugin_registry.get_plugins(), + "STATIC_URL": django_settings.STATIC_URL, + "CACHE_TIMEOUT": settings.CACHE_TIMEOUT, + } + ) return context -@register.inclusion_tag('wiki/includes/form.html', takes_context=True) +@register.inclusion_tag("wiki/includes/form.html", takes_context=True) def wiki_form(context, form_obj): if not isinstance(form_obj, BaseForm): raise TypeError( - "Error including form, it's not a form, it's a %s" % - type(form_obj)) - context.update({'form': form_obj}) + "Error including form, it's not a form, it's a %s" % type(form_obj) + ) + context.update({"form": form_obj}) return context -@register.inclusion_tag('wiki/includes/messages.html', takes_context=True) +@register.inclusion_tag("wiki/includes/messages.html", takes_context=True) def wiki_messages(context): - messages = context.get('messages', []) + messages = context.get("messages", []) for message in messages: message.css_class = settings.MESSAGE_TAG_CSS_CLASS[message.level] - context.update({ - 'messages': messages - }) + context.update({"messages": messages}) return context @@ -116,22 +116,22 @@ def get_content_snippet(content, keyword, max_words=30): if match_position != -1: try: - match_start = content.rindex(' ', 0, match_position) + 1 + match_start = content.rindex(" ", 0, match_position) + 1 except ValueError: match_start = 0 try: - match_end = content.index(' ', match_position + len(keyword)) + match_end = content.index(" ", match_position + len(keyword)) except ValueError: match_end = len(content) all_before = clean_text(content[:match_start]) match = content[match_start:match_end] all_after = clean_text(content[match_end:]) - before_words = all_before[-max_words // 2:] - after_words = all_after[:max_words - len(before_words)] + before_words = all_before[-max_words // 2 :] + after_words = all_after[: max_words - len(before_words)] before = " ".join(before_words) after = " ".join(after_words) html = ("%s %s %s" % (before, striptags(match), after)).strip() - kw_p = re.compile(r'(\S*%s\S*)' % keyword, re.IGNORECASE) + kw_p = re.compile(r"(\S*%s\S*)" % keyword, re.IGNORECASE) html = kw_p.sub(r"<strong>\1</strong>", html) return mark_safe(html) @@ -185,12 +185,12 @@ def is_locked(model): @register.simple_tag(takes_context=True) def login_url(context): - request = context['request'] - qs = request.META.get('QUERY_STRING', '') + request = context["request"] + qs = request.META.get("QUERY_STRING", "") if qs: - qs = urlquote('?' + qs) + qs = urlquote("?" + qs) else: - qs = '' + qs = "" return settings.LOGIN_URL + "?next=" + request.path + qs diff --git a/src/wiki/urls.py b/src/wiki/urls.py index 1a4b4bbd..b9d22e8b 100644 --- a/src/wiki/urls.py +++ b/src/wiki/urls.py @@ -6,7 +6,7 @@ from wiki.core.plugins import registry from wiki.views import accounts, article, deleted_list urlpatterns = [ - re_path(r'^', sites.site.urls), + re_path(r"^", sites.site.urls), ] @@ -63,48 +63,58 @@ class WikiURLPatterns: def get_root_urls(self): urlpatterns = [ - re_path(r'^$', + re_path( + r"^$", self.article_view_class.as_view(), - name='root', - kwargs={'path': ''}), - re_path(r'^create-root/$', - article.CreateRootView.as_view(), - name='root_create'), - re_path(r'^missing-root/$', + name="root", + kwargs={"path": ""}, + ), + re_path( + r"^create-root/$", article.CreateRootView.as_view(), name="root_create" + ), + re_path( + r"^missing-root/$", article.MissingRootView.as_view(), - name='root_missing'), - re_path(r'^_search/$', - self.search_view_class.as_view(), - name='search'), - re_path(r'^_revision/diff/(?P<revision_id>[0-9]+)/$', + name="root_missing", + ), + re_path(r"^_search/$", self.search_view_class.as_view(), name="search"), + re_path( + r"^_revision/diff/(?P<revision_id>[0-9]+)/$", self.article_diff_view_class.as_view(), - name='diff'), + name="diff", + ), ] return urlpatterns def get_deleted_list_urls(self): urlpatterns = [ - re_path('^_admin/$', - self.deleted_list_view_class.as_view(), - name="deleted_list"), + re_path( + "^_admin/$", self.deleted_list_view_class.as_view(), name="deleted_list" + ), ] return urlpatterns def get_accounts_urls(self): if settings.ACCOUNT_HANDLING: urlpatterns = [ - re_path(r'^_accounts/sign-up/$', + re_path( + r"^_accounts/sign-up/$", self.signup_view_class.as_view(), - name='signup'), - re_path(r'^_accounts/logout/$', + name="signup", + ), + re_path( + r"^_accounts/logout/$", self.logout_view_class.as_view(), - name='logout'), - re_path(r'^_accounts/login/$', - self.login_view_class.as_view(), - name='login'), - re_path(r'^_accounts/settings/$', + name="logout", + ), + re_path( + r"^_accounts/login/$", self.login_view_class.as_view(), name="login" + ), + re_path( + r"^_accounts/settings/$", self.profile_update_view_class.as_view(), - name='profile_update'), + name="profile_update", + ), ] else: urlpatterns = [] @@ -115,114 +125,164 @@ class WikiURLPatterns: # This one doesn't work because it don't know # where to redirect after... re_path( - r'^_revision/change/(?P<article_id>[0-9]+)/(?P<revision_id>[0-9]+)/$', + r"^_revision/change/(?P<article_id>[0-9]+)/(?P<revision_id>[0-9]+)/$", self.revision_change_view_class.as_view(), - name='change_revision'), - re_path(r'^_revision/preview/(?P<article_id>[0-9]+)/$', + name="change_revision", + ), + re_path( + r"^_revision/preview/(?P<article_id>[0-9]+)/$", self.article_preview_view_class.as_view(), - name='preview_revision'), + name="preview_revision", + ), re_path( - r'^_revision/merge/(?P<article_id>[0-9]+)/(?P<revision_id>[0-9]+)/preview/$', + r"^_revision/merge/(?P<article_id>[0-9]+)/(?P<revision_id>[0-9]+)/preview/$", self.revision_merge_view_class.as_view(preview=True), - name='merge_revision_preview'), + name="merge_revision_preview", + ), ] return urlpatterns def get_article_urls(self): urlpatterns = [ # Paths decided by article_ids - re_path(r'^(?P<article_id>[0-9]+)/$', + re_path( + r"^(?P<article_id>[0-9]+)/$", self.article_view_class.as_view(), - name='get'), - re_path(r'^(?P<article_id>[0-9]+)/delete/$', + name="get", + ), + re_path( + r"^(?P<article_id>[0-9]+)/delete/$", self.article_delete_view_class.as_view(), - name='delete'), - re_path(r'^(?P<article_id>[0-9]+)/deleted/$', + name="delete", + ), + re_path( + r"^(?P<article_id>[0-9]+)/deleted/$", self.article_deleted_view_class.as_view(), - name='deleted'), - re_path(r'^(?P<article_id>[0-9]+)/edit/$', + name="deleted", + ), + re_path( + r"^(?P<article_id>[0-9]+)/edit/$", self.article_edit_view_class.as_view(), - name='edit'), - re_path(r'^(?P<article_id>[0-9]+)/move/$', + name="edit", + ), + re_path( + r"^(?P<article_id>[0-9]+)/move/$", self.article_move_view_class.as_view(), - name='move'), - re_path(r'^(?P<article_id>[0-9]+)/preview/$', + name="move", + ), + re_path( + r"^(?P<article_id>[0-9]+)/preview/$", self.article_preview_view_class.as_view(), - name='preview'), - re_path(r'^(?P<article_id>[0-9]+)/history/$', + name="preview", + ), + re_path( + r"^(?P<article_id>[0-9]+)/history/$", self.article_history_view_class.as_view(), - name='history'), - re_path(r'^(?P<article_id>[0-9]+)/settings/$', + name="history", + ), + re_path( + r"^(?P<article_id>[0-9]+)/settings/$", self.article_settings_view_class.as_view(), - name='settings'), - re_path(r'^(?P<article_id>[0-9]+)/source/$', + name="settings", + ), + re_path( + r"^(?P<article_id>[0-9]+)/source/$", self.article_source_view_class.as_view(), - name='source'), + name="source", + ), re_path( - r'^(?P<article_id>[0-9]+)/revision/change/(?P<revision_id>[0-9]+)/$', + r"^(?P<article_id>[0-9]+)/revision/change/(?P<revision_id>[0-9]+)/$", self.revision_change_view_class.as_view(), - name='change_revision'), + name="change_revision", + ), re_path( - r'^(?P<article_id>[0-9]+)/revision/merge/(?P<revision_id>[0-9]+)/$', + r"^(?P<article_id>[0-9]+)/revision/merge/(?P<revision_id>[0-9]+)/$", self.revision_merge_view_class.as_view(), - name='merge_revision'), - re_path(r'^(?P<article_id>[0-9]+)/plugin/(?P<slug>\w+)/$', + name="merge_revision", + ), + re_path( + r"^(?P<article_id>[0-9]+)/plugin/(?P<slug>\w+)/$", self.article_plugin_view_class.as_view(), - name='plugin'), + name="plugin", + ), ] return urlpatterns def get_article_path_urls(self): urlpatterns = [ # Paths decided by URLs - re_path(r'^(?P<path>.+/|)_create/$', + re_path( + r"^(?P<path>.+/|)_create/$", self.article_create_view_class.as_view(), - name='create'), - re_path(r'^(?P<path>.+/|)_delete/$', + name="create", + ), + re_path( + r"^(?P<path>.+/|)_delete/$", self.article_delete_view_class.as_view(), - name='delete'), - re_path(r'^(?P<path>.+/|)_deleted/$', + name="delete", + ), + re_path( + r"^(?P<path>.+/|)_deleted/$", self.article_deleted_view_class.as_view(), - name='deleted'), - re_path(r'^(?P<path>.+/|)_edit/$', + name="deleted", + ), + re_path( + r"^(?P<path>.+/|)_edit/$", self.article_edit_view_class.as_view(), - name='edit'), - re_path(r'^(?P<path>.+/|)_move/$', + name="edit", + ), + re_path( + r"^(?P<path>.+/|)_move/$", self.article_move_view_class.as_view(), - name='move'), - re_path(r'^(?P<path>.+/|)_preview/$', + name="move", + ), + re_path( + r"^(?P<path>.+/|)_preview/$", self.article_preview_view_class.as_view(), - name='preview'), - re_path(r'^(?P<path>.+/|)_history/$', + name="preview", + ), + re_path( + r"^(?P<path>.+/|)_history/$", self.article_history_view_class.as_view(), - name='history'), - re_path(r'^(?P<path>.+/|)_dir/$', + name="history", + ), + re_path( + r"^(?P<path>.+/|)_dir/$", self.article_dir_view_class.as_view(), - name='dir'), - re_path(r'^(?P<path>.+/|)_search/$', + name="dir", + ), + re_path( + r"^(?P<path>.+/|)_search/$", self.search_view_class.as_view(), - name='search'), - re_path(r'^(?P<path>.+/|)_settings/$', + name="search", + ), + re_path( + r"^(?P<path>.+/|)_settings/$", self.article_settings_view_class.as_view(), - name='settings'), - re_path(r'^(?P<path>.+/|)_source/$', + name="settings", + ), + re_path( + r"^(?P<path>.+/|)_source/$", self.article_source_view_class.as_view(), - name='source'), + name="source", + ), re_path( - r'^(?P<path>.+/|)_revision/change/(?P<revision_id>[0-9]+)/$', + r"^(?P<path>.+/|)_revision/change/(?P<revision_id>[0-9]+)/$", self.revision_change_view_class.as_view(), - name='change_revision'), + name="change_revision", + ), re_path( - r'^(?P<path>.+/|)_revision/merge/(?P<revision_id>[0-9]+)/$', + r"^(?P<path>.+/|)_revision/merge/(?P<revision_id>[0-9]+)/$", self.revision_merge_view_class.as_view(), - name='merge_revision'), - re_path(r'^(?P<path>.+/|)_plugin/(?P<slug>\w+)/$', + name="merge_revision", + ), + re_path( + r"^(?P<path>.+/|)_plugin/(?P<slug>\w+)/$", self.article_plugin_view_class.as_view(), - name='plugin'), + name="plugin", + ), # This should always go last! - re_path(r'^(?P<path>.+/|)$', - self.article_view_class.as_view(), - name='get'), + re_path(r"^(?P<path>.+/|)$", self.article_view_class.as_view(), name="get"), ] return urlpatterns @@ -230,18 +290,22 @@ class WikiURLPatterns: def get_plugin_urls(): urlpatterns = [] for plugin in registry.get_plugins().values(): - slug = getattr(plugin, 'slug', None) + slug = getattr(plugin, "slug", None) if slug: - article_urlpatterns = plugin.urlpatterns.get('article', []) + article_urlpatterns = plugin.urlpatterns.get("article", []) urlpatterns += [ - re_path(r'^(?P<article_id>[0-9]+)/plugin/' + slug + '/', - include(article_urlpatterns)), - re_path(r'^(?P<path>.+/|)_plugin/' + slug + '/', - include(article_urlpatterns)), + re_path( + r"^(?P<article_id>[0-9]+)/plugin/" + slug + "/", + include(article_urlpatterns), + ), + re_path( + r"^(?P<path>.+/|)_plugin/" + slug + "/", + include(article_urlpatterns), + ), ] - root_urlpatterns = plugin.urlpatterns.get('root', []) + root_urlpatterns = plugin.urlpatterns.get("root", []) urlpatterns += [ - re_path(r'^_plugin/' + slug + '/', include(root_urlpatterns)), + re_path(r"^_plugin/" + slug + "/", include(root_urlpatterns)), ] return urlpatterns @@ -253,18 +317,19 @@ def get_pattern(app_name="wiki", namespace="wiki", url_config_class=None): https://docs.djangoproject.com/en/dev/topics/http/urls/#topics-http-reversing-url-namespaces """ import warnings + warnings.warn( "wiki.urls.get_pattern is deprecated and will be removed in next version, just `include('wiki.urls')` in your urlconf", - DeprecationWarning + DeprecationWarning, ) if url_config_class is None: - url_config_classname = getattr(settings, 'URL_CONFIG_CLASS', None) + url_config_classname = getattr(settings, "URL_CONFIG_CLASS", None) if url_config_classname is None: url_config_class = WikiURLPatterns else: warnings.warn( "URL_CONFIG_CLASS is deprecated and will be removed in next version, override `wiki.sites.WikiSite` instead", - DeprecationWarning + DeprecationWarning, ) url_config_class = import_string(url_config_classname) urlpatterns = url_config_class().get_urls() diff --git a/src/wiki/views/accounts.py b/src/wiki/views/accounts.py index 31890b0f..6b7c9f9d 100644 --- a/src/wiki/views/accounts.py +++ b/src/wiki/views/accounts.py @@ -11,7 +11,11 @@ SETTINGS.LOGOUT_URL from django.conf import settings as django_settings from django.contrib import messages -from django.contrib.auth import get_user_model, login as auth_login, logout as auth_logout +from django.contrib.auth import ( + get_user_model, + login as auth_login, + logout as auth_logout, +) from django.contrib.auth.forms import AuthenticationForm from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse @@ -31,32 +35,31 @@ class Signup(CreateView): def dispatch(self, request, *args, **kwargs): # Let logged in super users continue if not request.user.is_anonymous and not request.user.is_superuser: - return redirect('wiki:root') + return redirect("wiki:root") # If account handling is disabled, don't go here if not settings.ACCOUNT_HANDLING: return redirect(settings.SIGNUP_URL) # Allow superusers to use signup page... if not request.user.is_superuser and not settings.ACCOUNT_SIGNUP_ALLOWED: - c = {'error_msg': _('Account signup is only allowed for administrators.')} + c = {"error_msg": _("Account signup is only allowed for administrators.")} return render(request, "wiki/error.html", context=c) return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['honeypot_class'] = context['form'].honeypot_class - context['honeypot_jsfunction'] = context['form'].honeypot_jsfunction + context["honeypot_class"] = context["form"].honeypot_class + context["honeypot_jsfunction"] = context["form"].honeypot_jsfunction return context def get_success_url(self, *args): messages.success( - self.request, - _('You are now signed up... and now you can sign in!')) + self.request, _("You are now signed up... and now you can sign in!") + ) return reverse("wiki:login") class Logout(View): - def dispatch(self, request, *args, **kwargs): if not settings.ACCOUNT_HANDLING: return redirect(settings.LOGOUT_URL) @@ -75,7 +78,7 @@ class Login(FormView): def dispatch(self, request, *args, **kwargs): if not request.user.is_anonymous: - return redirect('wiki:root') + return redirect("wiki:root") if not settings.ACCOUNT_HANDLING: return redirect(settings.LOGIN_URL) return super().dispatch(request, *args, **kwargs) @@ -83,23 +86,23 @@ class Login(FormView): def get_form_kwargs(self): self.request.session.set_test_cookie() kwargs = super().get_form_kwargs() - kwargs['request'] = self.request + kwargs["request"] = self.request return kwargs def post(self, request, *args, **kwargs): - self.referer = request.session.get('login_referer', '') + self.referer = request.session.get("login_referer", "") return super().post(request, *args, **kwargs) def get(self, request, *args, **kwargs): - self.referer = request.META.get('HTTP_REFERER', '') - request.session['login_referer'] = self.referer + self.referer = request.META.get("HTTP_REFERER", "") + request.session["login_referer"] = self.referer return super().get(request, *args, **kwargs) def form_valid(self, form, *args, **kwargs): auth_login(self.request, form.get_user()) messages.info(self.request, _("You are now logged in! Have fun!")) if self.request.GET.get("next", None): - return redirect(self.request.GET['next']) + return redirect(self.request.GET["next"]) if django_settings.LOGIN_REDIRECT_URL: return redirect(django_settings.LOGIN_REDIRECT_URL) else: @@ -120,12 +123,12 @@ class Update(UpdateView): """ Save the initial referer """ - self.referer = request.META.get('HTTP_REFERER', '') - request.session['login_referer'] = self.referer + self.referer = request.META.get("HTTP_REFERER", "") + request.session["login_referer"] = self.referer return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): - self.referer = request.session.get('login_referer', '') + self.referer = request.session.get("login_referer", "") return super().post(request, *args, **kwargs) def form_valid(self, form): diff --git a/src/wiki/views/article.py b/src/wiki/views/article.py index e63e2faa..1a1cee35 100644 --- a/src/wiki/views/article.py +++ b/src/wiki/views/article.py @@ -12,7 +12,14 @@ from django.urls import reverse from django.utils.decorators import method_decorator from django.utils.translation import gettext as _, ngettext from django.views.decorators.clickjacking import xframe_options_sameorigin -from django.views.generic import DetailView, FormView, ListView, RedirectView, TemplateView, View +from django.views.generic import ( + DetailView, + FormView, + ListView, + RedirectView, + TemplateView, + View, +) from wiki import editors, forms, models from wiki.conf import settings from wiki.core import permissions @@ -36,7 +43,7 @@ class ArticleView(ArticleMixin, TemplateView): return super().dispatch(request, article, *args, **kwargs) def get_context_data(self, **kwargs): - kwargs['selected_tab'] = 'view' + kwargs["selected_tab"] = "view" return ArticleMixin.get_context_data(self, **kwargs) @@ -55,18 +62,22 @@ class Create(FormView, ArticleMixin): if form_class is None: form_class = self.get_form_class() kwargs = self.get_form_kwargs() - initial = kwargs.get('initial', {}) - initial['slug'] = self.request.GET.get('slug', None) - kwargs['initial'] = initial + initial = kwargs.get("initial", {}) + initial["slug"] = self.request.GET.get("slug", None) + kwargs["initial"] = initial form = form_class(self.request, self.urlpath, **kwargs) - form.fields['slug'].widget = forms.TextInputPrepend( - prepend='/' + self.urlpath.path, + form.fields["slug"].widget = forms.TextInputPrepend( + prepend="/" + self.urlpath.path, attrs={ # Make patterns force lowercase if we are case insensitive to bless the user with a # bit of strictness, anyways - 'pattern': '[a-z0-9_-]+' if not settings.URL_CASE_SENSITIVE else '[a-zA-Z0-9_-]+', - 'title': 'Lowercase letters, numbers, hyphens and underscores' if not settings.URL_CASE_SENSITIVE else 'Letters, numbers, hyphens and underscores', - } + "pattern": "[a-z0-9_-]+" + if not settings.URL_CASE_SENSITIVE + else "[a-zA-Z0-9_-]+", + "title": "Lowercase letters, numbers, hyphens and underscores" + if not settings.URL_CASE_SENSITIVE + else "Letters, numbers, hyphens and underscores", + }, ) return form @@ -76,39 +87,42 @@ class Create(FormView, ArticleMixin): self.request, self.article, self.urlpath, - form.cleaned_data['slug'], - form.cleaned_data['title'], - form.cleaned_data['content'], - form.cleaned_data['summary'] + form.cleaned_data["slug"], + form.cleaned_data["title"], + form.cleaned_data["content"], + form.cleaned_data["summary"], ) messages.success( self.request, - _("New article '%s' created.") % - self.newpath.article.current_revision.title) + _("New article '%s' created.") + % self.newpath.article.current_revision.title, + ) # TODO: Handle individual exceptions better and give good feedback. except Exception as e: log.exception("Exception creating article.") if self.request.user.is_superuser: messages.error( self.request, - _("There was an error creating this article: %s") % - str(e)) + _("There was an error creating this article: %s") % str(e), + ) else: - messages.error(self.request, _("There was an error creating this article.")) - return redirect('wiki:get', '') + messages.error( + self.request, _("There was an error creating this article.") + ) + return redirect("wiki:get", "") return self.get_success_url() def get_success_url(self): - return redirect('wiki:get', self.newpath.path) + return redirect("wiki:get", self.newpath.path) def get_context_data(self, **kwargs): c = ArticleMixin.get_context_data(self, **kwargs) - c['form'] = self.get_form() - c['parent_urlpath'] = self.urlpath - c['parent_article'] = self.article - c['create_form'] = c.pop('form', None) - c['editor'] = editors.getEditor() + c["form"] = self.get_form() + c["parent_urlpath"] = self.urlpath + c["parent_article"] = self.article + c["create_form"] = c.pop("form", None) + c["editor"] = editors.getEditor() return c @@ -124,15 +138,12 @@ class Delete(FormView, ArticleMixin): def dispatch1(self, request, article, *args, **kwargs): """Deleted view needs to access this method without a decorator, therefore it is separate.""" - urlpath = kwargs.get('urlpath', None) + urlpath = kwargs.get("urlpath", None) # Where to go after deletion... self.next = "" self.cannot_delete_root = False if urlpath and urlpath.parent: - self.next = reverse( - 'wiki:get', - kwargs={ - 'path': urlpath.parent.path}) + self.next = reverse("wiki:get", kwargs={"path": urlpath.parent.path}) elif urlpath: # We are a urlpath with no parent. This is the root self.cannot_delete_root = True @@ -141,7 +152,8 @@ class Delete(FormView, ArticleMixin): for art_obj in article.articleforobject_set.filter(is_mptt=True): if art_obj.content_object.parent: self.next = reverse( - 'wiki:get', kwargs={'article_id': art_obj.content_object.parent.article.id} + "wiki:get", + kwargs={"article_id": art_obj.content_object.parent.article.id}, ) else: self.cannot_delete_root = True @@ -149,24 +161,24 @@ class Delete(FormView, ArticleMixin): return super().dispatch(request, article, *args, **kwargs) def get_initial(self): - return {'revision': self.article.current_revision} + return {"revision": self.article.current_revision} def get_form(self, form_class=None): form = super().get_form(form_class=form_class) if self.article.can_moderate(self.request.user): - form.fields['purge'].widget = forms.forms.CheckboxInput() + form.fields["purge"].widget = forms.forms.CheckboxInput() return form def get_form_kwargs(self): kwargs = super().get_form_kwargs() - kwargs['article'] = self.article - kwargs['has_children'] = bool(self.children_slice) + kwargs["article"] = self.article + kwargs["has_children"] = bool(self.children_slice) return kwargs def form_valid(self, form): cd = form.cleaned_data - purge = cd['purge'] + purge = cd["purge"] # If we are purging, only moderators can delete articles with children cannot_delete_children = False @@ -177,8 +189,11 @@ class Delete(FormView, ArticleMixin): if self.cannot_delete_root or cannot_delete_children: messages.error( self.request, - _('This article cannot be deleted because it has children or is a root article.')) - return redirect('wiki:get', article_id=self.article.id) + _( + "This article cannot be deleted because it has children or is a root article." + ), + ) + return redirect("wiki:get", article_id=self.article.id) if can_moderate and purge: # First, remove children @@ -187,7 +202,10 @@ class Delete(FormView, ArticleMixin): self.article.delete() messages.success( self.request, - _('This article together with all its contents are now completely gone! Thanks!')) + _( + "This article together with all its contents are now completely gone! Thanks!" + ), + ) else: revision = models.ArticleRevision() revision.inherit_predecessor(self.article) @@ -196,8 +214,11 @@ class Delete(FormView, ArticleMixin): self.article.add_revision(revision) messages.success( self.request, - _('The article "%s" is now marked as deleted! Thanks for keeping the site free from unwanted material!') % - revision.title) + _( + 'The article "%s" is now marked as deleted! Thanks for keeping the site free from unwanted material!' + ) + % revision.title, + ) return self.get_success_url() def get_success_url(self): @@ -208,24 +229,25 @@ class Delete(FormView, ArticleMixin): if self.children_slice and not self.article.can_moderate(self.request.user): cannot_delete_children = True - kwargs['delete_form'] = self.get_form() - kwargs['form'] = kwargs['delete_form'] - kwargs['cannot_delete_root'] = self.cannot_delete_root - kwargs['delete_children'] = self.children_slice[:20] - kwargs['delete_children_more'] = len(self.children_slice) > 20 - kwargs['cannot_delete_children'] = cannot_delete_children + kwargs["delete_form"] = self.get_form() + kwargs["form"] = kwargs["delete_form"] + kwargs["cannot_delete_root"] = self.cannot_delete_root + kwargs["delete_children"] = self.children_slice[:20] + kwargs["delete_children_more"] = len(self.children_slice) > 20 + kwargs["cannot_delete_children"] = cannot_delete_children return super().get_context_data(**kwargs) class Edit(ArticleMixin, FormView): """Edit an article and process sidebar plugins.""" + form_class = forms.EditForm template_name = "wiki/edit.html" @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): - self.orig_content = kwargs.pop('content', None) + self.orig_content = kwargs.pop("content", None) self.sidebar_plugins = plugin_registry.get_sidebar() self.sidebar = [] return super().dispatch(request, article, *args, **kwargs) @@ -233,9 +255,8 @@ class Edit(ArticleMixin, FormView): def get_initial(self): initial = FormView.get_initial(self) - for field_name in ['title', 'content']: - session_key = 'unsaved_article_%s_%d' % ( - field_name, self.article.id) + for field_name in ["title", "content"]: + session_key = "unsaved_article_%s_%d" % (field_name, self.article.id) if session_key in self.request.session: content = self.request.session[session_key] initial[field_name] = content @@ -250,11 +271,14 @@ class Edit(ArticleMixin, FormView): if form_class is None: form_class = self.get_form_class() kwargs = self.get_form_kwargs() - if self.request.POST.get('save', '') != '1' and self.request.POST.get('preview') != '1': - kwargs['data'] = None - kwargs['files'] = None - kwargs['no_clean'] = True - kwargs['content'] = self.orig_content + if ( + self.request.POST.get("save", "") != "1" + and self.request.POST.get("preview") != "1" + ): + kwargs["data"] = None + kwargs["files"] = None + kwargs["no_clean"] = True + kwargs["content"] = self.orig_content return form_class(self.request, self.article.current_revision, **kwargs) def get_sidebar_form_classes(self): @@ -263,8 +287,9 @@ class Edit(ArticleMixin, FormView): to identify which form is being saved.""" form_classes = {} for cnt, plugin in enumerate(self.sidebar_plugins): - form_classes['form%d' % cnt] = ( - plugin, plugin.sidebar.get('form_class', None) + form_classes["form%d" % cnt] = ( + plugin, + plugin.sidebar.get("form_class", None), ) return form_classes @@ -274,7 +299,7 @@ class Edit(ArticleMixin, FormView): for form_id, (plugin, Form) in self.get_sidebar_form_classes().items(): if Form: form = Form(self.article, self.request.user) - setattr(form, 'form_id', form_id) + setattr(form, "form_id", form_id) else: form = None self.sidebar.append((plugin, form)) @@ -285,12 +310,13 @@ class Edit(ArticleMixin, FormView): self.sidebar_forms = [] for form_id, (plugin, Form) in self.get_sidebar_form_classes().items(): if Form: - if form_id == self.request.GET.get('f', None): + if form_id == self.request.GET.get("f", None): form = Form( self.article, self.request, data=self.request.POST, - files=self.request.FILES) + files=self.request.FILES, + ) if form.is_valid(): form.save() usermessage = form.get_usermessage() @@ -298,28 +324,38 @@ class Edit(ArticleMixin, FormView): messages.success(self.request, usermessage) else: messages.success( - self.request, - _('Your changes were saved.')) + self.request, _("Your changes were saved.") + ) - title = form.cleaned_data['unsaved_article_title'] - content = form.cleaned_data['unsaved_article_content'] + title = form.cleaned_data["unsaved_article_title"] + content = form.cleaned_data["unsaved_article_content"] orig_content = self.orig_content if not orig_content: orig_content = self.article.current_revision.content - if title != self.article.current_revision.title or content != orig_content: - request.session['unsaved_article_title_%d' % self.article.id] = title - request.session['unsaved_article_content_%d' % self.article.id] = content + if ( + title != self.article.current_revision.title + or content != orig_content + ): + request.session[ + "unsaved_article_title_%d" % self.article.id + ] = title + request.session[ + "unsaved_article_content_%d" % self.article.id + ] = content messages.warning( request, - _('Please note that your article text has not yet been saved!')) + _( + "Please note that your article text has not yet been saved!" + ), + ) if self.urlpath: - return redirect('wiki:edit', path=self.urlpath.path) - return redirect('wiki:edit', article_id=self.article.id) + return redirect("wiki:edit", path=self.urlpath.path) + return redirect("wiki:edit", article_id=self.article.id) else: form = Form(self.article, self.request) - setattr(form, 'form_id', form_id) + setattr(form, "form_id", form_id) else: form = None self.sidebar.append((plugin, form)) @@ -330,29 +366,29 @@ class Edit(ArticleMixin, FormView): (does not concern any sidebar forms!).""" revision = models.ArticleRevision() revision.inherit_predecessor(self.article) - revision.title = form.cleaned_data['title'] - revision.content = form.cleaned_data['content'] - revision.user_message = form.cleaned_data['summary'] + revision.title = form.cleaned_data["title"] + revision.content = form.cleaned_data["content"] + revision.user_message = form.cleaned_data["summary"] revision.deleted = False revision.set_from_request(self.request) self.article.add_revision(revision) messages.success( - self.request, - _('A new revision of the article was successfully added.')) + self.request, _("A new revision of the article was successfully added.") + ) return self.get_success_url() def get_success_url(self): """Go to the article view page when the article has been saved""" if self.urlpath: return redirect("wiki:get", path=self.urlpath.path) - return redirect('wiki:get', article_id=self.article.id) + return redirect("wiki:get", article_id=self.article.id) def get_context_data(self, **kwargs): - kwargs['form'] = self.get_form() - kwargs['edit_form'] = kwargs['form'] - kwargs['editor'] = editors.getEditor() - kwargs['selected_tab'] = 'edit' - kwargs['sidebar'] = self.sidebar + kwargs["form"] = self.get_form() + kwargs["edit_form"] = kwargs["form"] + kwargs["editor"] = editors.getEditor() + kwargs["selected_tab"] = "edit" + kwargs["sidebar"] = self.sidebar return super().get_context_data(**kwargs) @@ -373,8 +409,8 @@ class Move(ArticleMixin, FormView): return form_class(**kwargs) def get_context_data(self, **kwargs): - kwargs['form'] = self.get_form() - kwargs['root_path'] = models.URLPath.root() + kwargs["form"] = self.get_form() + kwargs["root_path"] = models.URLPath.root() return super().get_context_data(**kwargs) @transaction.atomic @@ -382,13 +418,12 @@ class Move(ArticleMixin, FormView): if not self.urlpath.parent: messages.error( self.request, - _('This article cannot be moved because it is a root article.') + _("This article cannot be moved because it is a root article."), ) - return redirect('wiki:get', article_id=self.article.id) + return redirect("wiki:get", article_id=self.article.id) dest_path = get_object_or_404( - models.URLPath, - pk=form.cleaned_data['destination'] + models.URLPath, pk=form.cleaned_data["destination"] ) tmp_path = dest_path @@ -396,9 +431,9 @@ class Move(ArticleMixin, FormView): if tmp_path == self.urlpath: messages.error( self.request, - _('This article cannot be moved to a child of itself.') + _("This article cannot be moved to a child of itself."), ) - return redirect('wiki:move', article_id=self.article.id) + return redirect("wiki:move", article_id=self.article.id) tmp_path = tmp_path.parent # Clear cache to update article lists (Old links) @@ -409,25 +444,28 @@ class Move(ArticleMixin, FormView): old_path = self.urlpath.path self.urlpath.parent = dest_path - self.urlpath.slug = form.cleaned_data['slug'] + self.urlpath.slug = form.cleaned_data["slug"] self.urlpath.save() # Reload url path form database self.urlpath = models.URLPath.objects.get(pk=self.urlpath.pk) # Use a copy of ourself (to avoid cache) and update article links again - for ancestor in models.Article.objects.get(pk=self.article.pk).ancestor_objects(): + for ancestor in models.Article.objects.get( + pk=self.article.pk + ).ancestor_objects(): ancestor.article.clear_cache() # Create a redirect page for every moved article # /old-slug # /old-slug/child # /old-slug/child/grand-child - if form.cleaned_data['redirect']: + if form.cleaned_data["redirect"]: # NB! Includes self! - descendants = list(self.urlpath.get_descendants( - include_self=True).order_by("level")) + descendants = list( + self.urlpath.get_descendants(include_self=True).order_by("level") + ) root_len = len(descendants[0].path) @@ -440,8 +478,8 @@ class Move(ArticleMixin, FormView): src_path = urljoin(old_path, dst_path[root_len:]) src_len = len(src_path) pos = src_path.rfind("/", 0, src_len - 1) - slug = src_path[pos + 1:src_len - 1] - parent_urlpath = models.URLPath.get_by_path(src_path[0:max(pos, 0)]) + slug = src_path[pos + 1 : src_len - 1] + parent_urlpath = models.URLPath.get_by_path(src_path[0 : max(pos, 0)]) link = "[wiki:/{path}](wiki:/{path})".format(path=dst_path) urlpath_new = models.URLPath._create_urlpath_from_request( @@ -461,14 +499,12 @@ class Move(ArticleMixin, FormView): ngettext( "Article successfully moved! Created {n} redirect.", "Article successfully moved! Created {n} redirects.", - len(descendants) - ).format( - n=len(descendants) - ) + len(descendants), + ).format(n=len(descendants)), ) else: - messages.success(self.request, _('Article successfully moved!')) + messages.success(self.request, _("Article successfully moved!")) return redirect("wiki:get", path=self.urlpath.path) @@ -483,26 +519,27 @@ class Deleted(Delete): @method_decorator(get_article(can_read=True, deleted_contents=True)) def dispatch(self, request, article, *args, **kwargs): - self.urlpath = kwargs.get('urlpath', None) + self.urlpath = kwargs.get("urlpath", None) self.article = article if self.urlpath: deleted_ancestor = self.urlpath.first_deleted_ancestor() if deleted_ancestor is None: # No one is deleted! - return redirect('wiki:get', path=self.urlpath.path) + return redirect("wiki:get", path=self.urlpath.path) elif deleted_ancestor != self.urlpath: # An ancestor was deleted, so redirect to that deleted page - return redirect('wiki:deleted', path=deleted_ancestor.path) + return redirect("wiki:deleted", path=deleted_ancestor.path) else: if not article.current_revision.deleted: - return redirect('wiki:get', article_id=article.id) + return redirect("wiki:get", article_id=article.id) # Restore - if request.GET.get('restore', False): + if request.GET.get("restore", False): can_restore = not article.current_revision.locked and article.can_delete( - request.user) + request.user + ) can_restore = can_restore or article.can_moderate(request.user) if can_restore: @@ -510,28 +547,29 @@ class Deleted(Delete): revision.inherit_predecessor(self.article) revision.set_from_request(request) revision.deleted = False - revision.automatic_log = _('Restoring article') + revision.automatic_log = _("Restoring article") self.article.add_revision(revision) messages.success( request, - _('The article "%s" and its children are now restored.') % - revision.title) + _('The article "%s" and its children are now restored.') + % revision.title, + ) if self.urlpath: - return redirect('wiki:get', path=self.urlpath.path) + return redirect("wiki:get", path=self.urlpath.path) else: - return redirect('wiki:get', article_id=article.id) + return redirect("wiki:get", article_id=article.id) return super().dispatch1(request, article, *args, **kwargs) def get_initial(self): return { - 'revision': self.article.current_revision, - 'purge': True, + "revision": self.article.current_revision, + "purge": True, } def get_context_data(self, **kwargs): - kwargs['purge_form'] = self.get_form() - kwargs['form'] = kwargs['purge_form'] + kwargs["purge_form"] = self.get_form() + kwargs["form"] = kwargs["purge_form"] return super().get_context_data(**kwargs) @@ -543,7 +581,7 @@ class Source(ArticleMixin, TemplateView): return super().dispatch(request, article, *args, **kwargs) def get_context_data(self, **kwargs): - kwargs['selected_tab'] = 'source' + kwargs["selected_tab"] = "source" return super().get_context_data(**kwargs) @@ -551,13 +589,14 @@ class History(ListView, ArticleMixin): template_name = "wiki/history.html" allow_empty = True - context_object_name = 'revisions' + context_object_name = "revisions" paginator_class = WikiPaginator paginate_by = 10 def get_queryset(self): - return models.ArticleRevision.objects.filter( - article=self.article).order_by('-created') + return models.ArticleRevision.objects.filter(article=self.article).order_by( + "-created" + ) def get_context_data(self, **kwargs): # Is this a bit of a hack? Use better inheritance? @@ -565,7 +604,7 @@ class History(ListView, ArticleMixin): kwargs_listview = ListView.get_context_data(self, **kwargs) kwargs.update(kwargs_article) kwargs.update(kwargs_listview) - kwargs['selected_tab'] = 'history' + kwargs["selected_tab"] = "history" return kwargs @method_decorator(get_article(can_read=True)) @@ -577,7 +616,7 @@ class Dir(ListView, ArticleMixin): template_name = "wiki/dir.html" allow_empty = True - context_object_name = 'directory' + context_object_name = "directory" model = models.URLPath paginator_class = WikiPaginator paginate_by = 30 @@ -586,7 +625,7 @@ class Dir(ListView, ArticleMixin): def dispatch(self, request, article, *args, **kwargs): self.filter_form = forms.DirFilterForm(request.GET) if self.filter_form.is_valid(): - self.query = self.filter_form.cleaned_data['query'] + self.query = self.filter_form.cleaned_data["query"] else: self.query = None return super().dispatch(request, article, *args, **kwargs) @@ -595,12 +634,14 @@ class Dir(ListView, ArticleMixin): children = self.urlpath.get_children().can_read(self.request.user) if self.query: children = children.filter( - Q(article__current_revision__title__icontains=self.query) | Q(slug__icontains=self.query) + Q(article__current_revision__title__icontains=self.query) + | Q(slug__icontains=self.query) ) if not self.article.can_moderate(self.request.user): children = children.active() children = children.select_related_common().order_by( - 'article__current_revision__title') + "article__current_revision__title" + ) return children def get_context_data(self, **kwargs): @@ -608,8 +649,8 @@ class Dir(ListView, ArticleMixin): kwargs_listview = ListView.get_context_data(self, **kwargs) kwargs.update(kwargs_article) kwargs.update(kwargs_listview) - kwargs['filter_query'] = self.query - kwargs['filter_form'] = self.filter_form + kwargs["filter_query"] = self.query + kwargs["filter_form"] = self.filter_form # Update each child's ancestor cache so the lookups don't have # to be repeated. @@ -635,47 +676,48 @@ class SearchView(ListView): return redirect(settings.LOGIN_URL) self.search_form = forms.SearchForm(request.GET) if self.search_form.is_valid(): - self.query = self.search_form.cleaned_data['q'] + self.query = self.search_form.cleaned_data["q"] else: self.query = None return super().dispatch(request, *args, **kwargs) def get_queryset(self): if not self.query: - return models.Article.objects.none().order_by('-current_revision__created') + return models.Article.objects.none().order_by("-current_revision__created") articles = models.Article.objects - path = self.kwargs.get('path', None) + path = self.kwargs.get("path", None) if path: try: self.urlpath = models.URLPath.get_by_path(path) article_ids = self.urlpath.get_descendants( - include_self=True).values_list('article_id') + include_self=True + ).values_list("article_id") articles = articles.filter(id__in=article_ids) except (NoRootURL, models.URLPath.DoesNotExist): raise Http404 articles = articles.filter( - Q(current_revision__title__icontains=self.query) | Q(current_revision__content__icontains=self.query) + Q(current_revision__title__icontains=self.query) + | Q(current_revision__content__icontains=self.query) ) if not permissions.can_moderate( - models.URLPath.root().article, - self.request.user): + models.URLPath.root().article, self.request.user + ): articles = articles.active().can_read(self.request.user) - return articles.order_by('-current_revision__created') + return articles.order_by("-current_revision__created") def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) - kwargs['search_form'] = self.search_form - kwargs['search_query'] = self.query - kwargs['urlpath'] = self.urlpath + kwargs["search_form"] = self.search_form + kwargs["search_query"] = self.query + kwargs["urlpath"] = self.urlpath return kwargs class Plugin(View): - def dispatch(self, request, path=None, slug=None, **kwargs): - kwargs['path'] = path + kwargs["path"] = path for plugin in list(plugin_registry.get_plugins().values()): - if getattr(plugin, 'slug', None) == slug: + if getattr(plugin, "slug", None) == slug: return plugin.article_view(request, **kwargs) raise Http404() @@ -704,14 +746,14 @@ class Settings(ArticleMixin, TemplateView): # TODO: Do not set an attribute on a form class - this # could be mixed up with a different instance # Use strategy from Edit view... - setattr(settings_forms[i], 'action', 'form%d' % i) + setattr(settings_forms[i], "action", "form%d" % i) return settings_forms def post(self, *args, **kwargs): self.forms = [] for form_class in self.get_form_classes(): - if form_class.action == self.request.GET.get('f', None): + if form_class.action == self.request.GET.get("f", None): form = form_class(self.article, self.request, self.request.POST) if form.is_valid(): form.save() @@ -719,8 +761,8 @@ class Settings(ArticleMixin, TemplateView): if usermessage: messages.success(self.request, usermessage) if self.urlpath: - return redirect('wiki:settings', path=self.urlpath.path) - return redirect('wiki:settings', article_id=self.article.id) + return redirect("wiki:settings", path=self.urlpath.path) + return redirect("wiki:settings", article_id=self.article.id) else: form = form_class(self.article, self.request) self.forms.append(form) @@ -740,12 +782,12 @@ class Settings(ArticleMixin, TemplateView): def get_success_url(self): if self.urlpath: - return redirect('wiki:settings', path=self.urlpath.path) - return redirect('wiki:settings', article_id=self.article.id) + return redirect("wiki:settings", path=self.urlpath.path) + return redirect("wiki:settings", article_id=self.article.id) def get_context_data(self, **kwargs): - kwargs['selected_tab'] = 'settings' - kwargs['forms'] = self.forms + kwargs["selected_tab"] = "settings" + kwargs["forms"] = self.forms return super().get_context_data(**kwargs) @@ -756,30 +798,30 @@ class ChangeRevisionView(RedirectView): @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): self.article = article - self.urlpath = kwargs.pop('kwargs', False) + self.urlpath = kwargs.pop("kwargs", False) self.change_revision() return super().dispatch(request, *args, **kwargs) def get_redirect_url(self, **kwargs): if self.urlpath: - return reverse("wiki:history", kwargs={'path': self.urlpath.path}) + return reverse("wiki:history", kwargs={"path": self.urlpath.path}) else: - return reverse('wiki:history', kwargs={'article_id': self.article.id}) + return reverse("wiki:history", kwargs={"article_id": self.article.id}) def change_revision(self): revision = get_object_or_404( - models.ArticleRevision, - article=self.article, - id=self.kwargs['revision_id']) + models.ArticleRevision, article=self.article, id=self.kwargs["revision_id"] + ) self.article.current_revision = revision self.article.save() messages.success( self.request, - _("The article %(title)s is now set to display revision #%(revision_number)d") % { - 'title': revision.title, - 'revision_number': revision.revision_number, - }) + _( + "The article %(title)s is now set to display revision #%(revision_number)d" + ) + % {"title": revision.title, "revision_number": revision.revision_number,}, + ) class Preview(ArticleMixin, TemplateView): @@ -789,7 +831,7 @@ class Preview(ArticleMixin, TemplateView): @method_decorator(xframe_options_sameorigin) @method_decorator(get_article(can_read=True, deleted_contents=True)) def dispatch(self, request, article, *args, **kwargs): - revision_id = request.GET.get('r', None) + revision_id = request.GET.get("r", None) self.title = None self.content = None self.preview = False @@ -801,19 +843,19 @@ class Preview(ArticleMixin, TemplateView): # querystring raise Http404() self.revision = get_object_or_404( - models.ArticleRevision, - article=article, - id=revision_id + models.ArticleRevision, article=article, id=revision_id ) else: self.revision = None return super().dispatch(request, article, *args, **kwargs) def post(self, request, *args, **kwargs): - edit_form = forms.EditForm(request, self.article.current_revision, request.POST, preview=True) + edit_form = forms.EditForm( + request, self.article.current_revision, request.POST, preview=True + ) if edit_form.is_valid(): - self.title = edit_form.cleaned_data['title'] - self.content = edit_form.cleaned_data['content'] + self.title = edit_form.cleaned_data["title"] + self.content = edit_form.cleaned_data["content"] self.preview = True return super().get(request, *args, **kwargs) @@ -825,16 +867,16 @@ class Preview(ArticleMixin, TemplateView): return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): - kwargs['title'] = self.title - kwargs['revision'] = self.revision - kwargs['content'] = self.content - kwargs['preview'] = self.preview + kwargs["title"] = self.title + kwargs["revision"] = self.revision + kwargs["content"] = self.content + kwargs["preview"] = self.preview return ArticleMixin.get_context_data(self, **kwargs) class DiffView(DetailView): model = models.ArticleRevision - pk_url_kwarg = 'revision_id' + pk_url_kwarg = "revision_id" def render_to_response(self, context, **response_kwargs): revision = self.get_object() @@ -850,10 +892,10 @@ class DiffView(DetailView): other_changes = [] if not other_revision or other_revision.title != revision.title: - other_changes.append((_('New title'), revision.title)) + other_changes.append((_("New title"), revision.title)) return object_to_json_response( - {'diff': list(diff), 'other_changes': other_changes} + {"diff": list(diff), "other_changes": other_changes} ) @@ -869,11 +911,12 @@ class MergeView(View): def get(self, request, article, revision_id, *args, **kwargs): revision = get_object_or_404( - models.ArticleRevision, - article=article, - id=revision_id) + models.ArticleRevision, article=article, id=revision_id + ) - current_text = article.current_revision.content if article.current_revision else "" + current_text = ( + article.current_revision.content if article.current_revision else "" + ) new_text = revision.content content = simple_merge(current_text, new_text) @@ -884,9 +927,9 @@ class MergeView(View): if revision.deleted: c = { - 'error_msg': _('You cannot merge with a deleted revision'), - 'article': article, - 'urlpath': self.urlpath + "error_msg": _("You cannot merge with a deleted revision"), + "article": article, + "urlpath": self.urlpath, } return render(request, self.template_error_name, context=c) @@ -896,41 +939,41 @@ class MergeView(View): new_revision.locked = False new_revision.title = article.current_revision.title new_revision.content = content - new_revision.automatic_log = ( - _('Merge between revision #%(r1)d and revision #%(r2)d') % { - 'r1': revision.revision_number, - 'r2': old_revision.revision_number}) + new_revision.automatic_log = _( + "Merge between revision #%(r1)d and revision #%(r2)d" + ) % {"r1": revision.revision_number, "r2": old_revision.revision_number} article.add_revision(new_revision, save=True) - old_revision.simpleplugin_set.all().update( - article_revision=new_revision) + old_revision.simpleplugin_set.all().update(article_revision=new_revision) revision.simpleplugin_set.all().update(article_revision=new_revision) messages.success( request, - _('A new revision was created: Merge between revision #%(r1)d and revision #%(r2)d') % { - 'r1': revision.revision_number, - 'r2': old_revision.revision_number}) + _( + "A new revision was created: Merge between revision #%(r1)d and revision #%(r2)d" + ) + % {"r1": revision.revision_number, "r2": old_revision.revision_number}, + ) if self.urlpath: - return redirect('wiki:edit', path=self.urlpath.path) + return redirect("wiki:edit", path=self.urlpath.path) else: - return redirect('wiki:edit', article_id=article.id) + return redirect("wiki:edit", article_id=article.id) c = { - 'article': article, - 'title': article.current_revision.title, - 'revision': None, - 'merge1': revision, - 'merge2': article.current_revision, - 'merge': True, - 'content': content + "article": article, + "title": article.current_revision.title, + "revision": None, + "merge1": revision, + "merge2": article.current_revision, + "merge": True, + "content": content, } return render(request, self.template_name, c) class CreateRootView(FormView): form_class = forms.CreateRootForm - template_name = 'wiki/create_root.html' + template_name = "wiki/create_root.html" def dispatch(self, request, *args, **kwargs): if not request.user.is_superuser: @@ -942,7 +985,7 @@ class CreateRootView(FormView): pass else: if root.article: - return redirect('wiki:get', path=root.path) + return redirect("wiki:get", path=root.path) # TODO: This is too dangerous... let's say there is no root.article and we end up here, # then it might cascade to delete a lot of things on an existing @@ -954,15 +997,15 @@ class CreateRootView(FormView): models.URLPath.create_root( title=form.cleaned_data["title"], content=form.cleaned_data["content"], - request=self.request + request=self.request, ) return redirect("wiki:root") def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) - kwargs['editor'] = editors.getEditor() + kwargs["editor"] = editors.getEditor() return kwargs class MissingRootView(TemplateView): - template_name = 'wiki/root_missing.html' + template_name = "wiki/root_missing.html" diff --git a/src/wiki/views/deleted_list.py b/src/wiki/views/deleted_list.py index 6068d5a7..b91b63b0 100644 --- a/src/wiki/views/deleted_list.py +++ b/src/wiki/views/deleted_list.py @@ -10,7 +10,7 @@ class DeletedListView(TemplateView): def dispatch(self, request, *args, **kwargs): # Let logged in super users continue if not request.user.is_superuser: - return redirect('wiki:root') + return redirect("wiki:root") return super().dispatch(request, *args, **kwargs) @@ -20,5 +20,5 @@ class DeletedListView(TemplateView): for article in article_list: if article.current_revision.deleted: deleted_articles.append(article) - kwargs['deleted_articles'] = deleted_articles + kwargs["deleted_articles"] = deleted_articles return super().get_context_data(**kwargs) diff --git a/src/wiki/views/mixins.py b/src/wiki/views/mixins.py index 35ac13b2..e46fd8e6 100644 --- a/src/wiki/views/mixins.py +++ b/src/wiki/views/mixins.py @@ -14,29 +14,30 @@ class ArticleMixin(TemplateResponseMixin): template context.""" def dispatch(self, request, article, *args, **kwargs): - self.urlpath = kwargs.pop('urlpath', None) + self.urlpath = kwargs.pop("urlpath", None) self.article = article self.children_slice = [] if settings.SHOW_MAX_CHILDREN > 0: try: for child in self.article.get_children( - max_num=settings.SHOW_MAX_CHILDREN + - 1, - articles__article__current_revision__deleted=False, - user_can_read=request.user): + max_num=settings.SHOW_MAX_CHILDREN + 1, + articles__article__current_revision__deleted=False, + user_can_read=request.user, + ): self.children_slice.append(child) except AttributeError as e: log.error( - "Attribute error most likely caused by wrong MPTT version. Use 0.5.3+.\n\n" + - str(e)) + "Attribute error most likely caused by wrong MPTT version. Use 0.5.3+.\n\n" + + str(e) + ) raise return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): - kwargs['urlpath'] = self.urlpath - kwargs['article'] = self.article - kwargs['article_tabs'] = registry.get_article_tabs() - kwargs['children_slice'] = self.children_slice[:20] - kwargs['children_slice_more'] = len(self.children_slice) > 20 - kwargs['plugins'] = registry.get_plugins() + kwargs["urlpath"] = self.urlpath + kwargs["article"] = self.article + kwargs["article_tabs"] = registry.get_article_tabs() + kwargs["children_slice"] = self.children_slice[:20] + kwargs["children_slice_more"] = len(self.children_slice) > 20 + kwargs["plugins"] = registry.get_plugins() return kwargs diff --git a/testproject/testproject/settings/base.py b/testproject/testproject/settings/base.py index ce967af1..b09f3994 100644 --- a/testproject/testproject/settings/base.py +++ b/testproject/testproject/settings/base.py @@ -19,7 +19,7 @@ BASE_DIR = os.path.dirname(PROJECT_DIR) # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'b^fv_)t39h%9p40)fnkfblo##jkr!$0)lkp6bpy!fi*f$4*92!' +SECRET_KEY = "b^fv_)t39h%9p40)fnkfblo##jkr!$0)lkp6bpy!fi*f$4*92!" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False @@ -28,54 +28,52 @@ ALLOWED_HOSTS = [] INSTALLED_APPS = [ - 'django.contrib.humanize.apps.HumanizeConfig', - 'django.contrib.auth.apps.AuthConfig', - 'django.contrib.contenttypes.apps.ContentTypesConfig', - 'django.contrib.sessions.apps.SessionsConfig', - 'django.contrib.sites.apps.SitesConfig', - 'django.contrib.messages.apps.MessagesConfig', - 'django.contrib.staticfiles.apps.StaticFilesConfig', - 'django.contrib.admin.apps.AdminConfig', - 'django.contrib.admindocs.apps.AdminDocsConfig', - 'sekizai', - 'sorl.thumbnail', + "django.contrib.humanize.apps.HumanizeConfig", + "django.contrib.auth.apps.AuthConfig", + "django.contrib.contenttypes.apps.ContentTypesConfig", + "django.contrib.sessions.apps.SessionsConfig", + "django.contrib.sites.apps.SitesConfig", + "django.contrib.messages.apps.MessagesConfig", + "django.contrib.staticfiles.apps.StaticFilesConfig", + "django.contrib.admin.apps.AdminConfig", + "django.contrib.admindocs.apps.AdminDocsConfig", + "sekizai", + "sorl.thumbnail", "django_nyt.apps.DjangoNytConfig", "wiki.apps.WikiConfig", "wiki.plugins.macros.apps.MacrosConfig", - 'wiki.plugins.help.apps.HelpConfig', - 'wiki.plugins.links.apps.LinksConfig', + "wiki.plugins.help.apps.HelpConfig", + "wiki.plugins.links.apps.LinksConfig", "wiki.plugins.images.apps.ImagesConfig", "wiki.plugins.attachments.apps.AttachmentsConfig", "wiki.plugins.notifications.apps.NotificationsConfig", - 'wiki.plugins.editsection.apps.EditSectionConfig', - 'wiki.plugins.globalhistory.apps.GlobalHistoryConfig', - 'mptt', + "wiki.plugins.editsection.apps.EditSectionConfig", + "wiki.plugins.globalhistory.apps.GlobalHistoryConfig", + "mptt", ] -TEST_RUNNER = 'django.test.runner.DiscoverRunner' +TEST_RUNNER = "django.test.runner.DiscoverRunner" MIDDLEWARE = [ - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.middleware.security.SecurityMiddleware", ] -ROOT_URLCONF = 'testproject.urls' +ROOT_URLCONF = "testproject.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - os.path.join(PROJECT_DIR, 'templates'), - ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(PROJECT_DIR, "templates"),], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ "django.contrib.auth.context_processors.auth", "django.template.context_processors.debug", "django.template.context_processors.i18n", @@ -84,23 +82,23 @@ TEMPLATES = [ "django.contrib.messages.context_processors.messages", "sekizai.context_processors.sekizai", ], - 'debug': DEBUG, + "debug": DEBUG, }, }, ] -WSGI_APPLICATION = 'testproject.wsgi.application' +WSGI_APPLICATION = "testproject.wsgi.application" -LOGIN_REDIRECT_URL = reverse_lazy('wiki:get', kwargs={'path': ''}) +LOGIN_REDIRECT_URL = reverse_lazy("wiki:get", kwargs={"path": ""}) # Database # https://docs.djangoproject.com/en/1.9/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(PROJECT_DIR, 'db', 'prepopulated.db'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(PROJECT_DIR, "db", "prepopulated.db"), } } @@ -109,27 +107,21 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",}, ] # Internationalization # https://docs.djangoproject.com/en/1.9/topics/i18n/ -TIME_ZONE = 'Europe/Berlin' +TIME_ZONE = "Europe/Berlin" # Language code for this installation. All choices can be found here: # http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-US' +LANGUAGE_CODE = "en-US" SITE_ID = 1 @@ -143,10 +135,10 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.9/howto/static-files/ -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(PROJECT_DIR, 'static') -MEDIA_ROOT = os.path.join(PROJECT_DIR, 'media') -MEDIA_URL = '/media/' +STATIC_URL = "/static/" +STATIC_ROOT = os.path.join(PROJECT_DIR, "static") +MEDIA_ROOT = os.path.join(PROJECT_DIR, "media") +MEDIA_URL = "/media/" WIKI_ANONYMOUS_WRITE = True diff --git a/testproject/testproject/settings/codehilite.py b/testproject/testproject/settings/codehilite.py index 03b519b2..27a54738 100644 --- a/testproject/testproject/settings/codehilite.py +++ b/testproject/testproject/settings/codehilite.py @@ -4,10 +4,5 @@ from testproject.settings.local import * # Test codehilite with pygments WIKI_MARKDOWN_KWARGS = { - 'extensions': [ - 'codehilite', - 'footnotes', - 'attr_list', - 'headerid', - 'extra', - ]} + "extensions": ["codehilite", "footnotes", "attr_list", "headerid", "extra",] +} diff --git a/testproject/testproject/settings/customauthuser.py b/testproject/testproject/settings/customauthuser.py index 01f8769b..80e1a4c7 100644 --- a/testproject/testproject/settings/customauthuser.py +++ b/testproject/testproject/settings/customauthuser.py @@ -3,17 +3,17 @@ import os # noqa @UnusedImport from .base import * # noqa @UnusedWildImport DATABASES = { - 'default': { + "default": { # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. - 'ENGINE': 'django.db.backends.sqlite3', + "ENGINE": "django.db.backends.sqlite3", # Or path to database file if using sqlite3. - 'NAME': os.path.join(PROJECT_DIR, 'db', 'prepopulated-customauthuser.db'), + "NAME": os.path.join(PROJECT_DIR, "db", "prepopulated-customauthuser.db"), } } INSTALLED_APPS = PROJECT_DIR + [ # Test application for testing custom users - 'wiki.tests.testdata', + "wiki.tests.testdata", ] -AUTH_USER_MODEL = 'testdata.CustomUser' +AUTH_USER_MODEL = "testdata.CustomUser" diff --git a/testproject/testproject/settings/dev.py b/testproject/testproject/settings/dev.py index 78a62a74..fcd42254 100644 --- a/testproject/testproject/settings/dev.py +++ b/testproject/testproject/settings/dev.py @@ -4,19 +4,20 @@ DEBUG = True for template_engine in TEMPLATES: - template_engine['OPTIONS']['debug'] = True + template_engine["OPTIONS"]["debug"] = True -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" try: import debug_toolbar # @UnusedImport + MIDDLEWARE = list(MIDDLEWARE) + [ - 'debug_toolbar.middleware.DebugToolbarMiddleware', + "debug_toolbar.middleware.DebugToolbarMiddleware", ] - INSTALLED_APPS = list(INSTALLED_APPS) + ['debug_toolbar'] - INTERNAL_IPS = ('127.0.0.1',) - DEBUG_TOOLBAR_CONFIG = {'INTERCEPT_REDIRECTS': False} + INSTALLED_APPS = list(INSTALLED_APPS) + ["debug_toolbar"] + INTERNAL_IPS = ("127.0.0.1",) + DEBUG_TOOLBAR_CONFIG = {"INTERCEPT_REDIRECTS": False} except ImportError: pass diff --git a/testproject/testproject/settings/sendfile.py b/testproject/testproject/settings/sendfile.py index 0d05286e..5f1e370e 100644 --- a/testproject/testproject/settings/sendfile.py +++ b/testproject/testproject/settings/sendfile.py @@ -1,10 +1,10 @@ from .base import * # noqa @UnusedWildImport -INSTALLED_APPS += ['sendfile'] +INSTALLED_APPS += ["sendfile"] WIKI_ATTACHMENTS_USE_SENDFILE = True -SENDFILE_BACKEND = 'sendfile.backends.development' +SENDFILE_BACKEND = "sendfile.backends.development" # SENDFILE_URL = None #Not needed # SENDFILE_ROOT = None #Not needed diff --git a/testproject/testproject/urls.py b/testproject/testproject/urls.py index a892cb78..fca7d43c 100644 --- a/testproject/testproject/urls.py +++ b/testproject/testproject/urls.py @@ -8,21 +8,25 @@ from django.urls import include, re_path admin.autodiscover() urlpatterns = [ - re_path(r'^admin/', admin.site.urls), - re_path(r'^robots.txt', lambda _: HttpResponse('User-agent: *\nDisallow: /')), + re_path(r"^admin/", admin.site.urls), + re_path(r"^robots.txt", lambda _: HttpResponse("User-agent: *\nDisallow: /")), ] if settings.DEBUG: urlpatterns += staticfiles_urlpatterns() urlpatterns += [ - re_path(r'^media/(?P<path>.*)$', static_serve, {'document_root': settings.MEDIA_ROOT}), + re_path( + r"^media/(?P<path>.*)$", + static_serve, + {"document_root": settings.MEDIA_ROOT}, + ), ] urlpatterns += [ - re_path(r'^notify/', include('django_nyt.urls')), - re_path(r'', include('wiki.urls')), + re_path(r"^notify/", include("django_nyt.urls")), + re_path(r"", include("wiki.urls")), ] -handler500 = 'testproject.views.server_error' -handler404 = 'testproject.views.page_not_found' +handler500 = "testproject.views.server_error" +handler404 = "testproject.views.page_not_found" diff --git a/testproject/testproject/views.py b/testproject/testproject/views.py index 0e8828b6..e5600b04 100644 --- a/testproject/testproject/views.py +++ b/testproject/testproject/views.py @@ -6,24 +6,24 @@ from django.views.decorators.csrf import requires_csrf_token @requires_csrf_token -def server_error(request, template_name='500.html', **param_dict): +def server_error(request, template_name="500.html", **param_dict): # You need to create a 500.html template. t = loader.get_template(template_name) - return HttpResponseServerError(t.render(RequestContext( - request, - { - 'MEDIA_URL': settings.MEDIA_URL, - 'STATIC_URL': settings.STATIC_URL, - 'request': request, - }, - ))) + return HttpResponseServerError( + t.render( + RequestContext( + request, + { + "MEDIA_URL": settings.MEDIA_URL, + "STATIC_URL": settings.STATIC_URL, + "request": request, + }, + ) + ) + ) -def page_not_found(request, template_name='404.html', exception=None): - response = server_error( - request, - template_name=template_name, - exception=exception - ) +def page_not_found(request, template_name="404.html", exception=None): + response = server_error(request, template_name=template_name, exception=exception) response.status_code = 404 return response diff --git a/tests/base.py b/tests/base.py index 46fc9086..44f2463b 100644 --- a/tests/base.py +++ b/tests/base.py @@ -8,22 +8,20 @@ from django.test import TestCase, override_settings from django.urls import reverse from wiki.models import URLPath -SUPERUSER1_USERNAME = 'admin' -SUPERUSER1_PASSWORD = 'secret' +SUPERUSER1_USERNAME = "admin" +SUPERUSER1_PASSWORD = "secret" class RequireSuperuserMixin: - def setUp(self): super().setUp() from django.contrib.auth import get_user_model + User = get_user_model() self.superuser1 = User.objects.create_superuser( - SUPERUSER1_USERNAME, - 'nobody@example.com', - SUPERUSER1_PASSWORD + SUPERUSER1_USERNAME, "nobody@example.com", SUPERUSER1_PASSWORD ) @@ -31,6 +29,7 @@ class RequireBasicData(RequireSuperuserMixin): """ Mixin that creates common data required for all tests. """ + pass @@ -39,7 +38,6 @@ class TestBase(RequireBasicData, TestCase): class RequireRootArticleMixin: - def setUp(self): super().setUp() self.root = URLPath.create_root() @@ -54,6 +52,7 @@ class ArticleTestBase(RequireRootArticleMixin, TestBase): """ Sets up basic data for testing with an article and some revisions """ + pass @@ -68,6 +67,7 @@ class WebTestCommonMixin(RequireBasicData, django_functest.ShortcutLoginMixin): """ Common setup required for WebTest and Selenium tests """ + def setUp(self): super().setUp() @@ -78,13 +78,15 @@ class WebTestBase(WebTestCommonMixin, django_functest.FuncWebTestMixin, TestCase pass -INCLUDE_SELENIUM_TESTS = os.environ.get('INCLUDE_SELENIUM_TESTS', '0') == '1' +INCLUDE_SELENIUM_TESTS = os.environ.get("INCLUDE_SELENIUM_TESTS", "0") == "1" @unittest.skipUnless(INCLUDE_SELENIUM_TESTS, "Skipping Selenium tests") -class SeleniumBase(WebTestCommonMixin, django_functest.FuncSeleniumMixin, StaticLiveServerTestCase): +class SeleniumBase( + WebTestCommonMixin, django_functest.FuncSeleniumMixin, StaticLiveServerTestCase +): driver_name = "Chrome" - display = os.environ.get('SELENIUM_SHOW_BROWSER', '0') == '1' + display = os.environ.get("SELENIUM_SHOW_BROWSER", "0") == "1" if not INCLUDE_SELENIUM_TESTS: # Don't call super() in setUpClass(), it will attempt to instantiate @@ -99,17 +101,15 @@ class SeleniumBase(WebTestCommonMixin, django_functest.FuncSeleniumMixin, Static class ArticleWebTestUtils: - def get_by_path(self, path): """ Get the article response for the path. Example: self.get_by_path("Level1/Slug2/").title """ - return self.client.get(reverse('wiki:get', kwargs={'path': path})) + return self.client.get(reverse("wiki:get", kwargs={"path": path})) class TemplateTestCase(TestCase): - @property def template(self): raise NotImplementedError("Subclasses must implement this") @@ -121,7 +121,6 @@ class TemplateTestCase(TestCase): # See # https://github.com/django-wiki/django-wiki/pull/382 class wiki_override_settings(override_settings): - def enable(self): super().enable() self.reload_wiki_settings() @@ -133,4 +132,5 @@ class wiki_override_settings(override_settings): def reload_wiki_settings(self): from importlib import reload from wiki.conf import settings + reload(settings) diff --git a/tests/core/test_accounts.py b/tests/core/test_accounts.py index 243c665e..3d7ceaff 100644 --- a/tests/core/test_accounts.py +++ b/tests/core/test_accounts.py @@ -7,49 +7,48 @@ from wiki.conf import settings as wiki_settings from wiki.models import reverse from ..base import ( - SUPERUSER1_PASSWORD, SUPERUSER1_USERNAME, ArticleWebTestUtils, DjangoClientTestBase, RequireRootArticleMixin, TestBase, wiki_override_settings, + SUPERUSER1_PASSWORD, + SUPERUSER1_USERNAME, + ArticleWebTestUtils, + DjangoClientTestBase, + RequireRootArticleMixin, + TestBase, + wiki_override_settings, ) from ..testdata.models import CustomUser -class AccountUpdateTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): - +class AccountUpdateTest( + RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase +): def test_password_change(self): """ Test that we can make a successful password change via the update form """ # Check out that it works as expected, notice that there is no referrer # on this GET request. - self.client.get( - resolve_url('wiki:profile_update',) - ) + self.client.get(resolve_url("wiki:profile_update",)) # Now check that we don't succeed with unmatching passwords example_data = { - 'password1': 'abcdef', - 'password2': 'abcdef123', - 'email': self.superuser1.email, + "password1": "abcdef", + "password2": "abcdef123", + "email": self.superuser1.email, } # save a new revision - response = self.client.post( - resolve_url('wiki:profile_update'), - example_data - ) + response = self.client.post(resolve_url("wiki:profile_update"), example_data) self.assertContains(response, "Passwords don't match", status_code=200) # Now check that we don't succeed with unmatching passwords example_data = { - 'password1': 'abcdef', - 'password2': 'abcdef', - 'email': self.superuser1.email, + "password1": "abcdef", + "password2": "abcdef", + "email": self.superuser1.email, } # save a new revision - response = self.client.post( - resolve_url('wiki:profile_update'), - example_data - ) + response = self.client.post(resolve_url("wiki:profile_update"), example_data) # Need to force str() because of: # TypeError: coercing to Unicode: need string or buffer, __proxy__ @@ -59,30 +58,29 @@ class AccountUpdateTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClie self.assertEqual( self.superuser1, authenticate( - username=self.superuser1.username, - password=example_data['password1'] - ) + username=self.superuser1.username, password=example_data["password1"] + ), ) -class UpdateProfileViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): - +class UpdateProfileViewTest( + RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase +): def test_update_profile(self): self.client.post( - resolve_url('wiki:profile_update'), + resolve_url("wiki:profile_update"), {"email": "test@test.com", "password1": "newPass", "password2": "newPass"}, - follow=True + follow=True, ) - test_auth = authenticate(username='admin', password='newPass') + test_auth = authenticate(username="admin", password="newPass") self.assertNotEqual(test_auth, None) - self.assertEqual(test_auth.email, 'test@test.com') + self.assertEqual(test_auth.email, "test@test.com") @wiki_override_settings(ACCOUNT_HANDLING=True) class LogoutViewTests(RequireRootArticleMixin, DjangoClientTestBase): - def test_logout_account_handling(self): self.client.get(wiki_settings.LOGOUT_URL) user = auth.get_user(self.client) @@ -95,27 +93,28 @@ class LoginTestViews(RequireRootArticleMixin, TestBase): def test_already_signed_in(self): self.client.force_login(self.superuser1) response = self.client.get(wiki_settings.LOGIN_URL) - self.assertRedirects(response, reverse('wiki:root')) + self.assertRedirects(response, reverse("wiki:root")) def test_log_in(self): self.client.post( wiki_settings.LOGIN_URL, - {'username': SUPERUSER1_USERNAME, 'password': SUPERUSER1_PASSWORD} + {"username": SUPERUSER1_USERNAME, "password": SUPERUSER1_PASSWORD}, ) self.assertIs(self.superuser1.is_authenticated, True) self.assertEqual(auth.get_user(self.client), self.superuser1) class SignupViewTests(RequireRootArticleMixin, TestBase): - @wiki_override_settings(ACCOUNT_HANDLING=True, ACCOUNT_SIGNUP_ALLOWED=True) def test_signup(self): response = self.client.post( wiki_settings.SIGNUP_URL, data={ - 'password1': 'wiki', 'password2': 'wiki', 'username': 'wiki', - 'email': 'wiki@wiki.com' - } + "password1": "wiki", + "password2": "wiki", + "username": "wiki", + "email": "wiki@wiki.com", + }, ) - self.assertIs(CustomUser.objects.filter(email='wiki@wiki.com').exists(), True) - self.assertRedirects(response, reverse('wiki:login')) + self.assertIs(CustomUser.objects.filter(email="wiki@wiki.com").exists(), True) + self.assertRedirects(response, reverse("wiki:login")) diff --git a/tests/core/test_basic.py b/tests/core/test_basic.py index a5697ba4..7ed2feff 100644 --- a/tests/core/test_basic.py +++ b/tests/core/test_basic.py @@ -12,7 +12,6 @@ from ..testdata.models import CustomGroup class URLPathTests(TestCase): - def test_manager(self): root = URLPath.create_root() @@ -23,22 +22,22 @@ class URLPathTests(TestCase): class CustomGroupTests(TestCase): - @wiki_override_settings(WIKI_GROUP_MODEL='auth.Group') + @wiki_override_settings(WIKI_GROUP_MODEL="auth.Group") def test_setting(self): - self.assertEqual(wiki_settings.GROUP_MODEL, 'auth.Group') + self.assertEqual(wiki_settings.GROUP_MODEL, "auth.Group") def test_custom(self): self.assertEqual(Group, CustomGroup) - self.assertEqual(wiki_settings.GROUP_MODEL, 'testdata.CustomGroup') + self.assertEqual(wiki_settings.GROUP_MODEL, "testdata.CustomGroup") class LineEndingsTests(TestCase): - def test_manager(self): article = Article() - article.add_revision(ArticleRevision(title="Root", content="Hello\nworld"), - save=True) + article.add_revision( + ArticleRevision(title="Root", content="Hello\nworld"), save=True + ) self.assertEqual("Hello\r\nworld", article.current_revision.content) @@ -51,6 +50,11 @@ class HttpTests(TestCase): assert "inline" in response.get("Content-Disposition") response = send_file(fabricate_request, fobject.name, filename="test.jpeg") assert response.has_header("Content-Disposition") - response = send_file(fabricate_request, fobject.name, filename="test.jpeg", last_modified=datetime.now()) + response = send_file( + fabricate_request, + fobject.name, + filename="test.jpeg", + last_modified=datetime.now(), + ) assert response.has_header("Content-Disposition") fobject.close() diff --git a/tests/core/test_checks.py b/tests/core/test_checks.py index bea9d82a..b33526c0 100644 --- a/tests/core/test_checks.py +++ b/tests/core/test_checks.py @@ -3,7 +3,12 @@ import copy from django.conf import settings from django.core.checks import Error, registry from django.test import TestCase -from wiki.checks import FIELDS_IN_CUSTOM_USER_MODEL, REQUIRED_CONTEXT_PROCESSORS, REQUIRED_INSTALLED_APPS, Tags +from wiki.checks import ( + FIELDS_IN_CUSTOM_USER_MODEL, + REQUIRED_CONTEXT_PROCESSORS, + REQUIRED_INSTALLED_APPS, + Tags, +) from ..base import wiki_override_settings @@ -18,27 +23,25 @@ class CheckTests(TestCase): with self.settings(INSTALLED_APPS=_remove(settings.INSTALLED_APPS, app[0])): errors = registry.run_checks(tags=[Tags.required_installed_apps]) expected_errors = [ - Error( - 'needs %s in INSTALLED_APPS' % app[1], - id='wiki.%s' % app[2], - ) + Error("needs %s in INSTALLED_APPS" % app[1], id="wiki.%s" % app[2],) ] self.assertEqual(errors, expected_errors) def test_required_context_processors(self): for context_processor in REQUIRED_CONTEXT_PROCESSORS: TEMPLATES = copy.deepcopy(settings.TEMPLATES) - TEMPLATES[0]['OPTIONS']['context_processors'] = [ + TEMPLATES[0]["OPTIONS"]["context_processors"] = [ cp - for cp in TEMPLATES[0]['OPTIONS']['context_processors'] + for cp in TEMPLATES[0]["OPTIONS"]["context_processors"] if cp != context_processor[0] ] with self.settings(TEMPLATES=TEMPLATES): errors = registry.run_checks(tags=[Tags.context_processors]) expected_errors = [ Error( - "needs %s in TEMPLATE['OPTIONS']['context_processors']" % context_processor[0], - id='wiki.%s' % context_processor[1], + "needs %s in TEMPLATE['OPTIONS']['context_processors']" + % context_processor[0], + id="wiki.%s" % context_processor[1], ) ] self.assertEqual(errors, expected_errors) @@ -54,28 +57,44 @@ class CheckTests(TestCase): from django.core.exceptions import FieldError from django import forms from ..testdata.models import VeryCustomUser - with self.assertRaisesRegex(FieldError, 'Unknown field\\(s\\) \\((email|username|, )+\\) specified for VeryCustomUser'): + + with self.assertRaisesRegex( + FieldError, + "Unknown field\\(s\\) \\((email|username|, )+\\) specified for VeryCustomUser", + ): + class UserUpdateForm(forms.ModelForm): class Meta: model = VeryCustomUser - fields = ['username', 'email'] + fields = ["username", "email"] def test_check_for_fields_in_custom_user_model(self): from django.contrib.auth import get_user_model - with wiki_override_settings(WIKI_ACCOUNT_HANDLING=False, AUTH_USER_MODEL='testdata.VeryCustomUser'): + + with wiki_override_settings( + WIKI_ACCOUNT_HANDLING=False, AUTH_USER_MODEL="testdata.VeryCustomUser" + ): errors = registry.run_checks(tags=[Tags.fields_in_custom_user_model]) self.assertEqual(errors, []) - with wiki_override_settings(WIKI_ACCOUNT_HANDLING=True, AUTH_USER_MODEL='testdata.VeryCustomUser'): + with wiki_override_settings( + WIKI_ACCOUNT_HANDLING=True, AUTH_USER_MODEL="testdata.VeryCustomUser" + ): errors = registry.run_checks(tags=[Tags.fields_in_custom_user_model]) expected_errors = [ Error( - '%s.%s.%s refers to a field that is not of type %s' % ( - get_user_model().__module__, get_user_model().__name__, field_fetcher, required_field_type), - hint='If you have your own login/logout views, turn off settings.WIKI_ACCOUNT_HANDLING', + "%s.%s.%s refers to a field that is not of type %s" + % ( + get_user_model().__module__, + get_user_model().__name__, + field_fetcher, + required_field_type, + ), + hint="If you have your own login/logout views, turn off settings.WIKI_ACCOUNT_HANDLING", obj=get_user_model(), - id='wiki.%s' % error_code, + id="wiki.%s" % error_code, ) - for check_function_name, field_fetcher, required_field_type, error_code in FIELDS_IN_CUSTOM_USER_MODEL] + for check_function_name, field_fetcher, required_field_type, error_code in FIELDS_IN_CUSTOM_USER_MODEL + ] self.assertEqual(errors, expected_errors) with wiki_override_settings(WIKI_ACCOUNT_HANDLING=True): errors = registry.run_checks(tags=[Tags.fields_in_custom_user_model]) diff --git a/tests/core/test_commands.py b/tests/core/test_commands.py index 3472c338..75787999 100644 --- a/tests/core/test_commands.py +++ b/tests/core/test_commands.py @@ -17,12 +17,12 @@ class TestManagementCommands(ArticleTestBase): def test_dumpdata_loaddata(self): sysout = sys.stdout - fixtures_file = tempfile.NamedTemporaryFile('w', delete=False, suffix=".json") + fixtures_file = tempfile.NamedTemporaryFile("w", delete=False, suffix=".json") sys.stdout = fixtures_file - call_command('dumpdata', 'wiki') + call_command("dumpdata", "wiki") fixtures_file.file.flush() fixtures_file.file.close() - sys.stdout = open(os.devnull, 'w') - call_command('loaddata', fixtures_file.name) + sys.stdout = open(os.devnull, "w") + call_command("loaddata", fixtures_file.name) sys.stdout = sysout os.unlink(fixtures_file.name) diff --git a/tests/core/test_forms.py b/tests/core/test_forms.py index 26bdbcf0..20c4c40b 100644 --- a/tests/core/test_forms.py +++ b/tests/core/test_forms.py @@ -6,21 +6,25 @@ from wiki.forms import DeleteForm, UserCreationForm class DeleteFormTests(RequireRootArticleMixin, DjangoClientTestBase): def test_not_sure(self): - data = {'purge': True, 'confirm': False} + data = {"purge": True, "confirm": False} form = DeleteForm(article=self.root_article, has_children=True, data=data) self.assertIs(form.is_valid(), False) - self.assertEqual(form.errors['__all__'], [gettext('You are not sure enough!')]) + self.assertEqual(form.errors["__all__"], [gettext("You are not sure enough!")]) class UserCreationFormTests(TestCase): def test_honeypot(self): data = { - 'address': 'Wiki Road 123', 'phone': '12345678', 'email': 'wiki@wiki.com', - 'username': 'WikiMan', 'password1': 'R@ndomString', 'password2': 'R@ndomString' + "address": "Wiki Road 123", + "phone": "12345678", + "email": "wiki@wiki.com", + "username": "WikiMan", + "password1": "R@ndomString", + "password2": "R@ndomString", } form = UserCreationForm(data=data) self.assertIs(form.is_valid(), False) self.assertEqual( - form.errors['__all__'], - ["Thank you, non-human visitor. Please keep trying to fill in the form."] + form.errors["__all__"], + ["Thank you, non-human visitor. Please keep trying to fill in the form."], ) diff --git a/tests/core/test_managers.py b/tests/core/test_managers.py index c53fa0cd..44209f1e 100644 --- a/tests/core/test_managers.py +++ b/tests/core/test_managers.py @@ -11,15 +11,10 @@ from ..base import ArticleTestBase class ArticleManagerTests(ArticleTestBase): - def test_queryset_methods_directly_on_manager(self): - self.assertEqual( - Article.objects.can_read(self.superuser1).count(), 1 - ) - self.assertEqual( - Article.objects.can_write(self.superuser1).count(), 1 - ) + self.assertEqual(Article.objects.can_read(self.superuser1).count(), 1) + self.assertEqual(Article.objects.can_write(self.superuser1).count(), 1) self.assertEqual(Article.objects.active().count(), 1) def test_mass_deletion(self): @@ -27,61 +22,40 @@ class ArticleManagerTests(ArticleTestBase): https://github.com/django-wiki/django-wiki/issues/857 """ Article.objects.all().delete() - self.assertEqual( - Article.objects.all().count(), 0 - ) + self.assertEqual(Article.objects.all().count(), 0) def test_queryset_methods_on_querysets(self): - self.assertEqual( - Article.objects.all().can_read(self.superuser1).count(), 1 - ) - self.assertEqual( - Article.objects.all().can_write(self.superuser1).count(), 1 - ) + self.assertEqual(Article.objects.all().can_read(self.superuser1).count(), 1) + self.assertEqual(Article.objects.all().can_write(self.superuser1).count(), 1) self.assertEqual(Article.objects.all().active().count(), 1) # See: https://code.djangoproject.com/ticket/22817 def test_queryset_empty_querysets(self): - self.assertEqual( - Article.objects.none().can_read(self.superuser1).count(), 0 - ) - self.assertEqual( - Article.objects.none().can_write(self.superuser1).count(), 0 - ) + self.assertEqual(Article.objects.none().can_read(self.superuser1).count(), 0) + self.assertEqual(Article.objects.none().can_write(self.superuser1).count(), 0) self.assertEqual(Article.objects.none().active().count(), 0) class AttachmentManagerTests(ArticleTestBase): - def test_queryset_methods_directly_on_manager(self): # Do the same for Attachment which uses ArtickeFkManager - self.assertEqual( - Attachment.objects.can_read(self.superuser1).count(), 0 - ) - self.assertEqual( - Attachment.objects.can_write(self.superuser1).count(), 0 - ) + self.assertEqual(Attachment.objects.can_read(self.superuser1).count(), 0) + self.assertEqual(Attachment.objects.can_write(self.superuser1).count(), 0) self.assertEqual(Attachment.objects.active().count(), 0) def test_queryset_methods_on_querysets(self): - self.assertEqual( - Attachment.objects.all().can_read(self.superuser1).count(), 0 - ) - self.assertEqual( - Attachment.objects.all().can_write(self.superuser1).count(), 0 - ) + self.assertEqual(Attachment.objects.all().can_read(self.superuser1).count(), 0) + self.assertEqual(Attachment.objects.all().can_write(self.superuser1).count(), 0) self.assertEqual(Attachment.objects.all().active().count(), 0) # See: https://code.djangoproject.com/ticket/22817 def test_queryset_empty_query_sets(self): - self.assertEqual( - Attachment.objects.none().can_read(self.superuser1).count(), 0 - ) + self.assertEqual(Attachment.objects.none().can_read(self.superuser1).count(), 0) self.assertEqual( Attachment.objects.none().can_write(self.superuser1).count(), 0 ) @@ -89,7 +63,6 @@ class AttachmentManagerTests(ArticleTestBase): class URLPathManagerTests(ArticleTestBase): - def test_related_manager_works_with_filters(self): root = URLPath.root() self.assertNotIn(root.id, [p.id for p in root.children.active()]) diff --git a/tests/core/test_markdown.py b/tests/core/test_markdown.py index 48b9cdd8..bc4276a2 100644 --- a/tests/core/test_markdown.py +++ b/tests/core/test_markdown.py @@ -11,17 +11,17 @@ from ..base import ArticleTestBase try: import pygments + pygments = True # NOQA except ImportError: pygments = False class ArticleMarkdownTests(ArticleTestBase): - - @patch('wiki.core.markdown.settings') + @patch("wiki.core.markdown.settings") def test_do_not_modify_extensions(self, settings): - extensions = ['footnotes', 'attr_list', 'sane_lists'] - settings.MARKDOWN_KWARGS = {'extensions': extensions} + extensions = ["footnotes", "attr_list", "sane_lists"] + settings.MARKDOWN_KWARGS = {"extensions": extensions} number_of_extensions = len(extensions) ArticleMarkdown(None) self.assertEqual(len(extensions), number_of_extensions) @@ -29,66 +29,54 @@ class ArticleMarkdownTests(ArticleTestBase): def test_html_removal(self): urlpath = URLPath.create_urlpath( - self.root, - 'html_removal', - title="Test 1", - content="</html>only_this" + self.root, "html_removal", title="Test 1", content="</html>only_this" ) self.assertEqual(urlpath.article.render(), "<p>only_this</p>") class ResponsiveTableExtensionTests(TestCase): - def setUp(self): super().setUp() - self.md = markdown.Markdown(extensions=[ - 'extra', - ResponsiveTableExtension() - ]) - self.md_without = markdown.Markdown(extensions=['extra']) + self.md = markdown.Markdown(extensions=["extra", ResponsiveTableExtension()]) + self.md_without = markdown.Markdown(extensions=["extra"]) def test_wrapping(self): - text = '|th|th|\n|--|--|\n|td|td|' - expected = '<div class="table-responsive">\n' + self.md_without.convert(text) + '\n</div>' + text = "|th|th|\n|--|--|\n|td|td|" + expected = ( + '<div class="table-responsive">\n' + + self.md_without.convert(text) + + "\n</div>" + ) self.assertEqual(self.md.convert(text), expected) class CodehiliteTests(TestCase): - def test_fenced_code(self): - md = markdown.Markdown( - extensions=['extra', WikiCodeHiliteExtension()] - ) - text = ( - "Code:\n" - "\n" - "```python\n" - "echo 'line 1'\n" - "echo 'line 2'\n" - "```\n" - ) + md = markdown.Markdown(extensions=["extra", WikiCodeHiliteExtension()]) + text = "Code:\n" "\n" "```python\n" "echo 'line 1'\n" "echo 'line 2'\n" "```\n" result = ( - """<p>Code:</p>\n""" - """<div class="codehilite-wrap"><div class="codehilite"><pre><span></span><span class="n">echo</span> <span class="s1">'line 1'</span>\n""" - """<span class="n">echo</span> <span class="s1">'line 2'</span>\n""" - """</pre></div>\n""" - """</div>""" - ) if pygments else ( - """<p>Code:</p>\n""" - """<div class="codehilite-wrap"><pre class="codehilite"><code class="language-python">echo 'line 1'\n""" - """echo 'line 2'</code></pre>\n""" - """</div>""" + ( + """<p>Code:</p>\n""" + """<div class="codehilite-wrap"><div class="codehilite"><pre><span></span><span class="n">echo</span> <span class="s1">'line 1'</span>\n""" + """<span class="n">echo</span> <span class="s1">'line 2'</span>\n""" + """</pre></div>\n""" + """</div>""" + ) + if pygments + else ( + """<p>Code:</p>\n""" + """<div class="codehilite-wrap"><pre class="codehilite"><code class="language-python">echo 'line 1'\n""" + """echo 'line 2'</code></pre>\n""" + """</div>""" + ) ) self.assertEqual( - md.convert(text), - result, + md.convert(text), result, ) def test_indented_code(self): - md = markdown.Markdown( - extensions=['extra', WikiCodeHiliteExtension()] - ) + md = markdown.Markdown(extensions=["extra", WikiCodeHiliteExtension()]) text = ( "Code:\n" "\n" @@ -99,25 +87,28 @@ class CodehiliteTests(TestCase): "\n" ) result = ( - """<p>Code:</p>\n""" - """<div class="codehilite-wrap"><table class="codehilitetable"><tr><td class="linenos"><div class="linenodiv"><pre>1\n""" - """2\n""" - """3\n""" - """4</pre></div></td><td class="code"><div class="codehilite"><pre><span></span><span class="ch">#!/usr/bin/python</span>\n""" - """<span class="k">print</span><span class="p">(</span><span class="s1">'line 1'</span><span class="p">)</span>\n""" - """<span class="k">print</span><span class="p">(</span><span class="s1">'line 2'</span><span class="p">)</span>\n""" - """<span class="k">print</span><span class="p">(</span><span class="s1">'æøå'</span><span class="p">)</span>\n""" - """</pre></div>\n""" - """</td></tr></table></div>""" - ) if pygments else ( - """<p>Code:</p>\n""" - """<div class="codehilite-wrap"><pre class="codehilite"><code class="language-python linenums">#!/usr/bin/python\n""" - """print('line 1')\n""" - """print('line 2')\n""" - """print('æøå')</code></pre>\n""" - """</div>""" + ( + """<p>Code:</p>\n""" + """<div class="codehilite-wrap"><table class="codehilitetable"><tr><td class="linenos"><div class="linenodiv"><pre>1\n""" + """2\n""" + """3\n""" + """4</pre></div></td><td class="code"><div class="codehilite"><pre><span></span><span class="ch">#!/usr/bin/python</span>\n""" + """<span class="k">print</span><span class="p">(</span><span class="s1">'line 1'</span><span class="p">)</span>\n""" + """<span class="k">print</span><span class="p">(</span><span class="s1">'line 2'</span><span class="p">)</span>\n""" + """<span class="k">print</span><span class="p">(</span><span class="s1">'æøå'</span><span class="p">)</span>\n""" + """</pre></div>\n""" + """</td></tr></table></div>""" + ) + if pygments + else ( + """<p>Code:</p>\n""" + """<div class="codehilite-wrap"><pre class="codehilite"><code class="language-python linenums">#!/usr/bin/python\n""" + """print('line 1')\n""" + """print('line 2')\n""" + """print('æøå')</code></pre>\n""" + """</div>""" + ) ) self.assertEqual( - md.convert(text), - result, + md.convert(text), result, ) diff --git a/tests/core/test_models.py b/tests/core/test_models.py index 91639490..aa823b85 100644 --- a/tests/core/test_models.py +++ b/tests/core/test_models.py @@ -13,27 +13,28 @@ Group = apps.get_model(settings.GROUP_MODEL) class WikiCustomUrlPatterns(WikiURLPatterns): - def get_article_urls(self): urlpatterns = [ - re_path('^my-wiki/(?P<article_id>[0-9]+)/$', + re_path( + "^my-wiki/(?P<article_id>[0-9]+)/$", self.article_view_class.as_view(), - name='get' - ), + name="get", + ), ] return urlpatterns def get_article_path_urls(self): urlpatterns = [ - re_path('^my-wiki/(?P<path>.+/|)$', + re_path( + "^my-wiki/(?P<path>.+/|)$", self.article_view_class.as_view(), - name='get'), + name="get", + ), ] return urlpatterns class ArticleModelTest(TestCase): - def test_default_fields_of_empty_article(self): a = Article.objects.create() @@ -57,7 +58,7 @@ class ArticleModelTest(TestCase): def test_str_method_if_have_current_revision(self): - title = 'Test title' + title = "Test title" a = Article.objects.create() ArticleRevision.objects.create(article=a, title=title) @@ -68,7 +69,7 @@ class ArticleModelTest(TestCase): a = Article.objects.create() - expected = 'Article without content (1)' + expected = "Article without content (1)" self.assertEqual(str(a), expected) @@ -80,16 +81,11 @@ class ArticleModelTest(TestCase): a2 = Article.objects.create() s2 = Site.objects.create(domain="somethingelse.com", name="somethingelse.com") - URLPath.objects.create( - article=a2, - site=s2, - parent=u1, - slug='test_slug' - ) + URLPath.objects.create(article=a2, site=s2, parent=u1, slug="test_slug") url = a2.get_absolute_url() - expected = '/test_slug/' + expected = "/test_slug/" self.assertEqual(url, expected) @@ -99,13 +95,13 @@ class ArticleModelTest(TestCase): url = a.get_absolute_url() - expected = '/1/' + expected = "/1/" self.assertEqual(url, expected) def test_article_is_related_to_articlerevision(self): - title = 'Test title' + title = "Test title" a = Article.objects.create() r = ArticleRevision.objects.create(article=a, title=title) @@ -115,7 +111,7 @@ class ArticleModelTest(TestCase): def test_article_is_related_to_owner(self): - u = User.objects.create(username='Noman', password='pass') + u = User.objects.create(username="Noman", password="pass") a = Article.objects.create(owner=u) self.assertEqual(a.owner, u) @@ -131,11 +127,6 @@ class ArticleModelTest(TestCase): def test_cache(self): a = Article.objects.create() - ArticleRevision.objects.create( - article=a, title="test", content="# header" - ) - expected = ( - """<h1 id="wiki-toc-header">header""" - """.*</h1>""" - ) + ArticleRevision.objects.create(article=a, title="test", content="# header") + expected = """<h1 id="wiki-toc-header">header""" """.*</h1>""" self.assertRegexpMatches(a.get_cached_content(), expected) diff --git a/tests/core/test_sites.py b/tests/core/test_sites.py index 06f7cc0e..796e4021 100644 --- a/tests/core/test_sites.py +++ b/tests/core/test_sites.py @@ -13,13 +13,17 @@ from ..base import wiki_override_settings class WikiCustomSite(sites.WikiSite): def get_article_urls(self): urlpatterns = [ - re_path('^some-prefix/(?P<article_id>[0-9]+)/$', self.article_view, name='get'), + re_path( + "^some-prefix/(?P<article_id>[0-9]+)/$", self.article_view, name="get" + ), ] return urlpatterns def get_article_path_urls(self): urlpatterns = [ - re_path('^some-other-prefix/(?P<path>.+/|)$', self.article_view, name='get'), + re_path( + "^some-other-prefix/(?P<path>.+/|)$", self.article_view, name="get" + ), ] return urlpatterns @@ -29,32 +33,32 @@ class WikiCustomConfig(WikiConfig): urlpatterns = [ - re_path(r'^notify/', include('django_nyt.urls')), - re_path(r'', include('wiki.urls')), + re_path(r"^notify/", include("django_nyt.urls")), + re_path(r"", include("wiki.urls")), ] @wiki_override_settings( INSTALLED_APPS=[ - 'tests.testdata', - 'django.contrib.auth.apps.AuthConfig', - 'django.contrib.contenttypes.apps.ContentTypesConfig', - 'django.contrib.sessions.apps.SessionsConfig', - 'django.contrib.admin.apps.AdminConfig', - 'django.contrib.humanize.apps.HumanizeConfig', - 'django.contrib.sites.apps.SitesConfig', - 'django_nyt.apps.DjangoNytConfig', - 'mptt', - 'sekizai', - 'sorl.thumbnail', - 'tests.core.test_sites.WikiCustomConfig', - 'wiki.plugins.attachments.apps.AttachmentsConfig', - 'wiki.plugins.notifications.apps.NotificationsConfig', - 'wiki.plugins.images.apps.ImagesConfig', - 'wiki.plugins.macros.apps.MacrosConfig', - 'wiki.plugins.globalhistory.apps.GlobalHistoryConfig', + "tests.testdata", + "django.contrib.auth.apps.AuthConfig", + "django.contrib.contenttypes.apps.ContentTypesConfig", + "django.contrib.sessions.apps.SessionsConfig", + "django.contrib.admin.apps.AdminConfig", + "django.contrib.humanize.apps.HumanizeConfig", + "django.contrib.sites.apps.SitesConfig", + "django_nyt.apps.DjangoNytConfig", + "mptt", + "sekizai", + "sorl.thumbnail", + "tests.core.test_sites.WikiCustomConfig", + "wiki.plugins.attachments.apps.AttachmentsConfig", + "wiki.plugins.notifications.apps.NotificationsConfig", + "wiki.plugins.images.apps.ImagesConfig", + "wiki.plugins.macros.apps.MacrosConfig", + "wiki.plugins.globalhistory.apps.GlobalHistoryConfig", ], - ROOT_URLCONF='tests.core.test_sites', + ROOT_URLCONF="tests.core.test_sites", ) class CustomWikiSiteTest(TestCase): def setUp(self): @@ -68,12 +72,12 @@ class CustomWikiSiteTest(TestCase): reload(urls) def test_use_custom_wiki_site(self): - self.assertEqual(sites.site.__class__.__name__, 'WikiCustomSite') + self.assertEqual(sites.site.__class__.__name__, "WikiCustomSite") def test_get_absolute_url_if_urlpath_set_is_not_exists__no_root_urlconf(self): a = Article.objects.create() - self.assertEqual(a.get_absolute_url(), '/some-prefix/1/') + self.assertEqual(a.get_absolute_url(), "/some-prefix/1/") def test_get_absolute_url_if_urlpath_set_is_exists__no_root_urlconf(self): a1 = Article.objects.create() @@ -82,11 +86,6 @@ class CustomWikiSiteTest(TestCase): a2 = Article.objects.create() s2 = Site.objects.create(domain="somethingelse.com", name="somethingelse.com") - URLPath.objects.create( - article=a2, - site=s2, - parent=u1, - slug='test_slug' - ) - - self.assertEqual(a2.get_absolute_url(), '/some-other-prefix/test_slug/') + URLPath.objects.create(article=a2, site=s2, parent=u1, slug="test_slug") + + self.assertEqual(a2.get_absolute_url(), "/some-other-prefix/test_slug/") diff --git a/tests/core/test_template_filters.py b/tests/core/test_template_filters.py index c84f55bd..eed51421 100644 --- a/tests/core/test_template_filters.py +++ b/tests/core/test_template_filters.py @@ -1,6 +1,13 @@ from django.contrib.auth import get_user_model from wiki.models import Article, ArticleRevision -from wiki.templatetags.wiki_tags import can_delete, can_moderate, can_read, can_write, get_content_snippet, is_locked +from wiki.templatetags.wiki_tags import ( + can_delete, + can_moderate, + can_read, + can_write, + get_content_snippet, + is_locked, +) from ..base import TemplateTestCase, wiki_override_settings @@ -15,121 +22,121 @@ class GetContentSnippet(TemplateTestCase): """ def test_keyword_at_the_end_of_the_content(self): - text = 'lorem ' * 80 - content = text + ' list' + text = "lorem " * 80 + content = text + " list" expected = ( - 'lorem lorem lorem lorem lorem lorem lorem lorem lorem ' - 'lorem lorem lorem lorem lorem lorem <strong>list</strong>' + "lorem lorem lorem lorem lorem lorem lorem lorem lorem " + "lorem lorem lorem lorem lorem lorem <strong>list</strong>" ) - output = get_content_snippet(content, 'list') + output = get_content_snippet(content, "list") self.assertEqual(output, expected) def test_keyword_at_the_beginning_of_the_content(self): - text = 'lorem ' * 80 - content = 'list ' + text + text = "lorem " * 80 + content = "list " + text expected = ( - '<strong>list</strong> lorem lorem lorem lorem lorem ' - 'lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem ' - 'lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem ' - 'lorem lorem lorem' + "<strong>list</strong> lorem lorem lorem lorem lorem " + "lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem " + "lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem " + "lorem lorem lorem" ) - output = get_content_snippet(content, 'list') + output = get_content_snippet(content, "list") self.assertEqual(output, expected) def test_whole_content_consists_of_keywords(self): - content = 'lorem ' * 80 - expected = '<strong>lorem</strong>' + 30 * ' <strong>lorem</strong>' + content = "lorem " * 80 + expected = "<strong>lorem</strong>" + 30 * " <strong>lorem</strong>" - output = get_content_snippet(content, 'lorem') + output = get_content_snippet(content, "lorem") self.assertEqual(output, expected) def test_keyword_is_not_in_a_content(self): - content = 'lorem ' * 80 + content = "lorem " * 80 expected = ( - 'lorem lorem lorem lorem lorem lorem lorem lorem lorem ' - 'lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem ' - 'lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem' + "lorem lorem lorem lorem lorem lorem lorem lorem lorem " + "lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem " + "lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem" ) - output = get_content_snippet(content, 'list') + output = get_content_snippet(content, "list") self.assertEqual(output, expected) def test_a_few_keywords_in_content(self): - text = 'lorem ' * 80 - content = 'list ' + text + text = "lorem " * 80 + content = "list " + text - text = 'ipsum ' * 80 - content += text + ' list ' + text = "ipsum " * 80 + content += text + " list " - text = 'dolorum ' * 80 - content += text + ' list' + text = "dolorum " * 80 + content += text + " list" - expected = '<strong>list</strong>' + 30 * ' lorem' + expected = "<strong>list</strong>" + 30 * " lorem" - output = get_content_snippet(content, 'list') + output = get_content_snippet(content, "list") self.assertEqual(output, expected) # XXX bug or feature? def test_keyword_is_in_content_and_max_words_is_zero(self): - text = 'spam ' * 800 - content = text + ' list' + text = "spam " * 800 + content = text + " list" - output = get_content_snippet(content, 'list', 0) - expected = 'spam ' * 800 + '<strong>list</strong>' + output = get_content_snippet(content, "list", 0) + expected = "spam " * 800 + "<strong>list</strong>" self.assertEqual(output, expected) # XXX bug or feature? def test_keyword_is_in_content_and_max_words_is_negative(self): - text = 'spam ' * 80 - content = text + ' list' + text = "spam " * 80 + content = text + " list" - output = get_content_snippet(content, 'list', -10) - expected = 'spam ' * 75 + '<strong>list</strong>' + output = get_content_snippet(content, "list", -10) + expected = "spam " * 75 + "<strong>list</strong>" self.assertEqual(output, expected) # XXX bug or feature? def test_keyword_is_not_in_content_and_max_words_is_zero(self): - content = 'spam ' * 15 + content = "spam " * 15 - output = get_content_snippet(content, 'list', 0) - expected = '' + output = get_content_snippet(content, "list", 0) + expected = "" self.assertEqual(output, expected) # XXX bug or feature? def test_keyword_is_not_in_content_and_max_words_is_negative(self): - content = 'spam ' * 15 + content = "spam " * 15 - output = get_content_snippet(content, 'list', -10) - expected = 'spam spam spam spam spam' + output = get_content_snippet(content, "list", -10) + expected = "spam spam spam spam spam" self.assertEqual(output, expected) def test_no_content(self): - content = '' + content = "" - output = get_content_snippet(content, 'list') + output = get_content_snippet(content, "list") - self.assertEqual(output, '') + self.assertEqual(output, "") - content = ' ' + content = " " - output = get_content_snippet(content, 'list') + output = get_content_snippet(content, "list") - self.assertEqual(output, '') + self.assertEqual(output, "") def test_strip_tags(self): - keyword = 'maybe' + keyword = "maybe" content = """ I should citate Shakespeare or Byron. @@ -138,9 +145,9 @@ class GetContentSnippet(TemplateTestCase): """ expected = ( - 'I should citate Shakespeare or Byron. ' - 'Or <strong>maybe</strong> copy paste from python ' - 'or django documentation. <strong>Maybe.</strong>' + "I should citate Shakespeare or Byron. " + "Or <strong>maybe</strong> copy paste from python " + "or django documentation. <strong>Maybe.</strong>" ) output = get_content_snippet(content, keyword, 30) @@ -149,13 +156,14 @@ class GetContentSnippet(TemplateTestCase): def test_max_words_arg(self): - keyword = 'eggs' + keyword = "eggs" content = """ knight eggs spam ham eggs guido python eggs circus """ - expected = ('knight <strong>eggs</strong> spam ham ' - '<strong>eggs</strong> guido') + expected = ( + "knight <strong>eggs</strong> spam ham " "<strong>eggs</strong> guido" + ) output = get_content_snippet(content, keyword, 5) @@ -164,15 +172,15 @@ class GetContentSnippet(TemplateTestCase): output = get_content_snippet(content, keyword, 0) expected = ( - 'knight <strong>eggs</strong> spam ham ' - '<strong>eggs</strong> guido python <strong>eggs</strong>' + "knight <strong>eggs</strong> spam ham " + "<strong>eggs</strong> guido python <strong>eggs</strong>" ) self.assertEqual(output, expected) def test_content_case_preserved(self): - keyword = 'DOlOr' - match = 'DoLoR' - content = 'lorem ipsum %s sit amet' % match + keyword = "DOlOr" + match = "DoLoR" + content = "lorem ipsum %s sit amet" % match output = get_content_snippet(content, keyword) self.assertIn(match, output) self.assertNotIn(keyword, output) @@ -190,25 +198,25 @@ class CanRead(TemplateTestCase): a = Article.objects.create() - u = User.objects.create(username='Nobody', password='pass') + u = User.objects.create(username="Nobody", password="pass") output = can_read(a, u) self.assertIs(output, True) - output = self.render({'article': a, 'user': u}) - self.assertIn('True', output) + output = self.render({"article": a, "user": u}) + self.assertIn("True", output) @wiki_override_settings(WIKI_CAN_READ=lambda *args: False) def test_user_dont_have_permission(self): a = Article.objects.create() - u = User.objects.create(username='Noman', password='pass') + u = User.objects.create(username="Noman", password="pass") output = can_read(a, u) self.assertIs(output, False) - output = self.render({'article': a, 'user': u}) - self.assertIn('False', output) + output = self.render({"article": a, "user": u}) + self.assertIn("False", output) class CanWrite(TemplateTestCase): @@ -223,25 +231,25 @@ class CanWrite(TemplateTestCase): a = Article.objects.create() - u = User.objects.create(username='Nobody', password='pass') + u = User.objects.create(username="Nobody", password="pass") output = can_write(a, u) self.assertIs(output, True) - output = self.render({'article': a, 'user': u}) - self.assertIn('True', output) + output = self.render({"article": a, "user": u}) + self.assertIn("True", output) @wiki_override_settings(WIKI_CAN_WRITE=lambda *args: False) def test_user_dont_have_permission(self): a = Article.objects.create() - u = User.objects.create(username='Noman', password='pass') + u = User.objects.create(username="Noman", password="pass") output = can_write(a, u) self.assertIs(output, False) - output = self.render({'article': a, 'user': u}) - self.assertIn('False', output) + output = self.render({"article": a, "user": u}) + self.assertIn("False", output) class CanDelete(TemplateTestCase): @@ -256,25 +264,25 @@ class CanDelete(TemplateTestCase): a = Article.objects.create() - u = User.objects.create(username='Nobody', password='pass') + u = User.objects.create(username="Nobody", password="pass") output = can_delete(a, u) self.assertIs(output, True) - output = self.render({'article': a, 'user': u}) - self.assertIn('True', output) + output = self.render({"article": a, "user": u}) + self.assertIn("True", output) @wiki_override_settings(WIKI_CAN_WRITE=lambda *args: False) def test_user_dont_have_permission(self): a = Article.objects.create() - u = User.objects.create(username='Noman', password='pass') + u = User.objects.create(username="Noman", password="pass") output = can_delete(a, u) self.assertIs(output, False) - output = self.render({'article': a, 'user': u}) - self.assertIn('False', output) + output = self.render({"article": a, "user": u}) + self.assertIn("False", output) class CanModerate(TemplateTestCase): @@ -289,24 +297,24 @@ class CanModerate(TemplateTestCase): a = Article.objects.create() - u = User.objects.create(username='Nobody', password='pass') + u = User.objects.create(username="Nobody", password="pass") output = can_moderate(a, u) self.assertIs(output, True) - output = self.render({'article': a, 'user': u}) - self.assertIn('True', output) + output = self.render({"article": a, "user": u}) + self.assertIn("True", output) def test_user_dont_have_permission(self): a = Article.objects.create() - u = User.objects.create(username='Noman', password='pass') + u = User.objects.create(username="Noman", password="pass") output = can_moderate(a, u) self.assertIs(output, False) - output = self.render({'article': a, 'user': u}) - self.assertIn('False', output) + output = self.render({"article": a, "user": u}) + self.assertIn("False", output) class IsLocked(TemplateTestCase): @@ -323,8 +331,8 @@ class IsLocked(TemplateTestCase): output = is_locked(a) self.assertIsNone(output) - output = self.render({'article': a}) - self.assertIn('None', output) + output = self.render({"article": a}) + self.assertIn("None", output) def test_have_current_revision_and_not_locked(self): @@ -340,8 +348,8 @@ class IsLocked(TemplateTestCase): output = is_locked(b) self.assertIs(output, False) - output = self.render({'article': a}) - self.assertIn('False', output) + output = self.render({"article": a}) + self.assertIn("False", output) def test_have_current_revision_and_locked(self): @@ -351,8 +359,8 @@ class IsLocked(TemplateTestCase): output = is_locked(a) self.assertIs(output, True) - output = self.render({'article': a}) - self.assertIn('True', output) + output = self.render({"article": a}) + self.assertIn("True", output) class PluginEnabled(TemplateTestCase): @@ -364,7 +372,7 @@ class PluginEnabled(TemplateTestCase): def test_true(self): output = self.render({}) - self.assertIn('It is enabled', output) + self.assertIn("It is enabled", output) class WikiSettings(TemplateTestCase): @@ -377,4 +385,4 @@ class WikiSettings(TemplateTestCase): @wiki_override_settings(WIKI_ACCOUNT_HANDLING=lambda *args: True) def test_setting(self): output = self.render({}) - self.assertIn('It is enabled', output) + self.assertIn("It is enabled", output) diff --git a/tests/core/test_template_tags.py b/tests/core/test_template_tags.py index f7cafefd..a072994b 100644 --- a/tests/core/test_template_tags.py +++ b/tests/core/test_template_tags.py @@ -7,7 +7,12 @@ from django.http import HttpRequest from wiki.conf import settings from wiki.forms import CreateRootForm from wiki.models import Article, ArticleForObject, ArticleRevision -from wiki.templatetags.wiki_tags import article_for_object, login_url, wiki_form, wiki_render +from wiki.templatetags.wiki_tags import ( + article_for_object, + login_url, + wiki_form, + wiki_render, +) from ..base import TemplateTestCase @@ -27,6 +32,7 @@ class ArticleForObjectTemplatetagTest(TemplateTestCase): def setUp(self): super().setUp() from wiki.templatetags import wiki_tags + wiki_tags._cache = {} def test_obj_arg_is_not_a_django_model(self): @@ -34,13 +40,13 @@ class ArticleForObjectTemplatetagTest(TemplateTestCase): from wiki.templatetags import wiki_tags with self.assertRaises(TypeError): - article_for_object({}, '') + article_for_object({}, "") with self.assertRaises(TypeError): - article_for_object({'request': 100500}, {}) + article_for_object({"request": 100500}, {}) with self.assertRaises(TypeError): - self.render({'obj': 'tiger!'}) + self.render({"obj": "tiger!"}) self.assertEqual(len(wiki_tags._cache), 0) @@ -55,7 +61,7 @@ class ArticleForObjectTemplatetagTest(TemplateTestCase): self.assertIsNone(cache[obj]) self.assertEqual(len(cache), 1) - self.render({'obj': obj}) + self.render({"obj": obj}) self.assertIn(obj, cache) self.assertIsNone(cache[obj]) @@ -67,9 +73,7 @@ class ArticleForObjectTemplatetagTest(TemplateTestCase): a = Article.objects.create() content_type = ContentType.objects.get_for_model(a) ArticleForObject.objects.create( - article=a, - content_type=content_type, - object_id=1 + article=a, content_type=content_type, object_id=1 ) output = article_for_object({}, a) @@ -79,7 +83,7 @@ class ArticleForObjectTemplatetagTest(TemplateTestCase): self.assertEqual(cache[a], a) self.assertEqual(len(cache), 1) - self.render({'obj': a}) + self.render({"obj": a}) self.assertIn(a, cache) self.assertEqual(cache[a], a) @@ -90,7 +94,8 @@ class ArticleForObjectTemplatetagTest(TemplateTestCase): model = Article.objects.create() from wiki.templatetags import wiki_tags - wiki_tags._cache = {model: 'spam'} + + wiki_tags._cache = {model: "spam"} article_for_object({}, model) @@ -98,26 +103,25 @@ class ArticleForObjectTemplatetagTest(TemplateTestCase): self.assertIsNone(wiki_tags._cache[model]) self.assertEqual(len(wiki_tags._cache), 1) - self.render({'obj': model}) + self.render({"obj": model}) self.assertIn(model, wiki_tags._cache) self.assertIsNone(wiki_tags._cache[model]) self.assertEqual(len(wiki_tags._cache), 1) - self.assertNotIn('spam', wiki_tags._cache.values()) + self.assertNotIn("spam", wiki_tags._cache.values()) def test_obj_in__cache_and_articleforobjec_is_exist(self): article = Article.objects.create() content_type = ContentType.objects.get_for_model(article) ArticleForObject.objects.create( - article=article, - content_type=content_type, - object_id=1 + article=article, content_type=content_type, object_id=1 ) from wiki.templatetags import wiki_tags - wiki_tags._cache = {article: 'spam'} + + wiki_tags._cache = {article: "spam"} output = article_for_object({}, article) @@ -125,12 +129,12 @@ class ArticleForObjectTemplatetagTest(TemplateTestCase): self.assertIn(article, wiki_tags._cache) self.assertEqual(wiki_tags._cache[article], article) - output = self.render({'obj': article}) + output = self.render({"obj": article}) self.assertIn(article, wiki_tags._cache) self.assertEqual(wiki_tags._cache[article], article) - expected = 'Article without content (1)' + expected = "Article without content (1)" self.assertIn(expected, output) @@ -145,22 +149,18 @@ class WikiRenderTest(TemplateTestCase): def tearDown(self): from wiki.core.plugins import registry + registry._cache = {} super().tearDown() - keys = ['article', - 'content', - 'preview', - 'plugins', - 'STATIC_URL', - 'CACHE_TIMEOUT' - ] + keys = ["article", "content", "preview", "plugins", "STATIC_URL", "CACHE_TIMEOUT"] def test_if_preview_content_is_none(self): # monkey patch from wiki.core.plugins import registry - registry._cache = {'ham': 'spam'} + + registry._cache = {"ham": "spam"} article = Article.objects.create() @@ -168,24 +168,22 @@ class WikiRenderTest(TemplateTestCase): self.assertCountEqual(self.keys, output) - self.assertEqual(output['article'], article) - self.assertIsNone(output['content']) - self.assertIs(output['preview'], False) + self.assertEqual(output["article"], article) + self.assertIsNone(output["content"]) + self.assertIs(output["preview"], False) - self.assertEqual(output['plugins'], {'ham': 'spam'}) - self.assertEqual(output['STATIC_URL'], django_settings.STATIC_URL) - self.assertEqual(output['CACHE_TIMEOUT'], settings.CACHE_TIMEOUT) + self.assertEqual(output["plugins"], {"ham": "spam"}) + self.assertEqual(output["STATIC_URL"], django_settings.STATIC_URL) + self.assertEqual(output["CACHE_TIMEOUT"], settings.CACHE_TIMEOUT) # Additional check - self.render({'article': article, 'pc': None}) + self.render({"article": article, "pc": None}) def test_called_with_preview_content_and_article_have_current_revision(self): article = Article.objects.create() ArticleRevision.objects.create( - article=article, - title="Test title", - content="Some beauty test text" + article=article, title="Test title", content="Some beauty test text" ) content = ( @@ -203,22 +201,22 @@ class WikiRenderTest(TemplateTestCase): # monkey patch from wiki.core.plugins import registry - registry._cache = {'spam': 'eggs'} + + registry._cache = {"spam": "eggs"} output = wiki_render({}, article, preview_content=content) self.assertCountEqual(self.keys, output) - self.assertEqual(output['article'], article) - self.assertRegexpMatches(output['content'], expected) - self.assertIs(output['preview'], True) - self.assertEqual(output['plugins'], {'spam': 'eggs'}) - self.assertEqual(output['STATIC_URL'], django_settings.STATIC_URL) - self.assertEqual(output['CACHE_TIMEOUT'], settings.CACHE_TIMEOUT) - - output = self.render({'article': article, 'pc': content}) + self.assertEqual(output["article"], article) + self.assertRegexpMatches(output["content"], expected) + self.assertIs(output["preview"], True) + self.assertEqual(output["plugins"], {"spam": "eggs"}) + self.assertEqual(output["STATIC_URL"], django_settings.STATIC_URL) + self.assertEqual(output["CACHE_TIMEOUT"], settings.CACHE_TIMEOUT) + + output = self.render({"article": article, "pc": content}) self.assertRegexpMatches(output, expected) - def test_called_with_preview_content_and_article_dont_have_current_revision( - self): + def test_called_with_preview_content_and_article_dont_have_current_revision(self): article = Article.objects.create() @@ -231,22 +229,23 @@ class WikiRenderTest(TemplateTestCase): # monkey patch from wiki.core.plugins import registry - registry._cache = {'spam': 'eggs'} + + registry._cache = {"spam": "eggs"} output = wiki_render({}, article, preview_content=content) self.assertCountEqual(self.keys, output) - self.assertEqual(output['article'], article) + self.assertEqual(output["article"], article) - self.assertMultiLineEqual(output['content'], '') - self.assertIs(output['preview'], True) + self.assertMultiLineEqual(output["content"], "") + self.assertIs(output["preview"], True) - self.assertEqual(output['plugins'], {'spam': 'eggs'}) - self.assertEqual(output['STATIC_URL'], django_settings.STATIC_URL) - self.assertEqual(output['CACHE_TIMEOUT'], settings.CACHE_TIMEOUT) + self.assertEqual(output["plugins"], {"spam": "eggs"}) + self.assertEqual(output["STATIC_URL"], django_settings.STATIC_URL) + self.assertEqual(output["CACHE_TIMEOUT"], settings.CACHE_TIMEOUT) - self.render({'article': article, 'pc': content}) + self.render({"article": article, "pc": content}) class WikiFormTest(TemplateTestCase): @@ -258,33 +257,33 @@ class WikiFormTest(TemplateTestCase): def test_form_obj_is_not_baseform_instance(self): - context = {'test_key': 'test_value'} - form_obj = 'ham' + context = {"test_key": "test_value"} + form_obj = "ham" with self.assertRaises(TypeError): wiki_form(context, form_obj) - self.assertEqual(context, {'test_key': 'test_value'}) + self.assertEqual(context, {"test_key": "test_value"}) with self.assertRaises(TypeError): - self.render({'test_key': 100500}) + self.render({"test_key": 100500}) - self.assertEqual(context, {'test_key': 'test_value'}) + self.assertEqual(context, {"test_key": "test_value"}) def test_form_obj_is_baseform_instance(self): - context = {'test_key': 'test_value'} + context = {"test_key": "test_value"} # not by any special reasons, just a form form_obj = CreateRootForm() wiki_form(context, form_obj) - self.assertEqual(context, {'test_key': 'test_value', 'form': form_obj}) + self.assertEqual(context, {"test_key": "test_value", "form": form_obj}) - self.render({'form_obj': form_obj}) + self.render({"form_obj": form_obj}) - self.assertEqual(context, {'test_key': 'test_value', 'form': form_obj}) + self.assertEqual(context, {"test_key": "test_value", "form": form_obj}) class LoginUrlTest(TemplateTestCase): @@ -307,51 +306,51 @@ class LoginUrlTest(TemplateTestCase): r = HttpRequest() r.META = {} - r.path = 'best/test/page/ever/' + r.path = "best/test/page/ever/" - output = login_url({'request': r}) + output = login_url({"request": r}) - expected = '/_accounts/login/?next=best/test/page/ever/' + expected = "/_accounts/login/?next=best/test/page/ever/" self.assertEqual(output, expected) - output = self.render({'request': r}) + output = self.render({"request": r}) self.assertIn(expected, output) def test_login_url_if_query_string_is_empty(self): r = HttpRequest() - r.META = {'QUERY_STRING': ''} - r.path = 'best/test/page/ever/' + r.META = {"QUERY_STRING": ""} + r.path = "best/test/page/ever/" - output = login_url({'request': r}) + output = login_url({"request": r}) - expected = '/_accounts/login/?next=best/test/page/ever/' + expected = "/_accounts/login/?next=best/test/page/ever/" self.assertEqual(output, expected) - output = self.render({'request': r}) + output = self.render({"request": r}) self.assertIn(expected, output) def test_login_url_if_query_string_is_not_empty(self): r = HttpRequest() - r.META = {'QUERY_STRING': 'title=Main_page&action=raw'} - r.path = 'best/test/page/ever/' + r.META = {"QUERY_STRING": "title=Main_page&action=raw"} + r.path = "best/test/page/ever/" - context = {'request': r} + context = {"request": r} output = login_url(context) expected = ( - '/_accounts/login/' - '?next=best/test/page/ever/%3Ftitle%3DMain_page%26action%3Draw' + "/_accounts/login/" + "?next=best/test/page/ever/%3Ftitle%3DMain_page%26action%3Draw" ) self.assertEqual(output, expected) - output = self.render({'request': r}) + output = self.render({"request": r}) self.assertIn(expected, output) diff --git a/tests/core/test_urls.py b/tests/core/test_urls.py index 6195b80d..ed85f051 100644 --- a/tests/core/test_urls.py +++ b/tests/core/test_urls.py @@ -8,41 +8,44 @@ from ..base import wiki_override_settings class WikiCustomUrlPatterns(WikiURLPatterns): - def get_article_urls(self): urlpatterns = [ - re_path('^some-prefix/(?P<article_id>[0-9]+)/$', + re_path( + "^some-prefix/(?P<article_id>[0-9]+)/$", self.article_view_class.as_view(), - name='get' - ), + name="get", + ), ] return urlpatterns def get_article_path_urls(self): urlpatterns = [ - re_path('^some-other-prefix/(?P<path>.+/|)$', + re_path( + "^some-other-prefix/(?P<path>.+/|)$", self.article_view_class.as_view(), - name='get'), + name="get", + ), ] return urlpatterns urlpatterns = [ - re_path(r'^notify/', include('django_nyt.urls')), - re_path(r'', get_wiki_pattern(url_config_class=WikiCustomUrlPatterns)) + re_path(r"^notify/", include("django_nyt.urls")), + re_path(r"", get_wiki_pattern(url_config_class=WikiCustomUrlPatterns)), ] -@wiki_override_settings(WIKI_URL_CONFIG_CLASS='tests.core.test_models.WikiCustomUrlPatterns', - ROOT_URLCONF='tests.core.test_urls') +@wiki_override_settings( + WIKI_URL_CONFIG_CLASS="tests.core.test_models.WikiCustomUrlPatterns", + ROOT_URLCONF="tests.core.test_urls", +) class ArticleModelReverseMethodTest(TestCase): - def test_get_absolute_url_if_urlpath_set_is_not_exists__no_root_urlconf(self): a = Article.objects.create() url = a.get_absolute_url() - expected = '/some-prefix/1/' + expected = "/some-prefix/1/" self.assertEqual(url, expected) @@ -54,15 +57,10 @@ class ArticleModelReverseMethodTest(TestCase): a2 = Article.objects.create() s2 = Site.objects.create(domain="somethingelse.com", name="somethingelse.com") - URLPath.objects.create( - article=a2, - site=s2, - parent=u1, - slug='test_slug' - ) + URLPath.objects.create(article=a2, site=s2, parent=u1, slug="test_slug") url = a2.get_absolute_url() - expected = '/some-other-prefix/test_slug/' + expected = "/some-other-prefix/test_slug/" self.assertEqual(url, expected) diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index 3ed2b4fc..389d5686 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -3,7 +3,6 @@ from wiki.core.utils import object_to_json_response class TestUtils(TestCase): - def test_object_to_json(self): """ Simple test, the actual serialization happens in json.dumps and we diff --git a/tests/core/test_views.py b/tests/core/test_views.py index 8ab4fe1c..d9e6c31c 100644 --- a/tests/core/test_views.py +++ b/tests/core/test_views.py @@ -12,7 +12,14 @@ from wiki import models from wiki.forms import PermissionsForm, validate_slug_numbers from wiki.models import ArticleRevision, URLPath, reverse -from ..base import SUPERUSER1_USERNAME, ArticleWebTestUtils, DjangoClientTestBase, RequireRootArticleMixin, SeleniumBase, WebTestBase +from ..base import ( + SUPERUSER1_USERNAME, + ArticleWebTestUtils, + DjangoClientTestBase, + RequireRootArticleMixin, + SeleniumBase, + WebTestBase, +) class RootArticleViewTestsBase(FuncBaseMixin): @@ -24,17 +31,16 @@ class RootArticleViewTestsBase(FuncBaseMixin): Test redirecting to /create-root/, creating the root article and a simple markup. """ - self.get_url('wiki:root') - self.assertUrlsEqual(resolve_url('wiki:root_create')) - self.fill({ - '#id_content': 'test heading h1\n====\n', - '#id_title': 'Wiki Test', - }) + self.get_url("wiki:root") + self.assertUrlsEqual(resolve_url("wiki:root_create")) + self.fill( + {"#id_content": "test heading h1\n====\n", "#id_title": "Wiki Test",} + ) self.submit('button[name="save_changes"]') - self.assertUrlsEqual('/') - self.assertTextPresent('test heading h1') + self.assertUrlsEqual("/") + self.assertTextPresent("test heading h1") article = URLPath.root().article - self.assertIn('test heading h1', article.current_revision.content) + self.assertIn("test heading h1", article.current_revision.content) class RootArticleViewTestsWebTest(RootArticleViewTestsBase, WebTestBase): @@ -45,37 +51,37 @@ class RootArticleViewTestsSelenium(RootArticleViewTestsBase, SeleniumBase): pass -class ArticleViewViewTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): +class ArticleViewViewTests( + RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase +): """ Tests for article views, assuming a root article already created. """ - def dump_db_status(self, message=''): + def dump_db_status(self, message=""): """Debug printing of the complete important database content.""" - print('*** db status *** {}'.format(message)) + print("*** db status *** {}".format(message)) from wiki.models import Article, ArticleRevision for klass in (Article, ArticleRevision, URLPath): - print('* {} *'.format(klass.__name__)) + print("* {} *".format(klass.__name__)) pprint.pprint(list(klass.objects.values()), width=240) def test_redirects_to_create_if_the_slug_is_unknown(self): - response = self.get_by_path('unknown/') + response = self.get_by_path("unknown/") self.assertRedirects( - response, - resolve_url('wiki:create', path='') + '?slug=unknown' + response, resolve_url("wiki:create", path="") + "?slug=unknown" ) def test_redirects_to_create_with_lowercased_slug(self): - response = self.get_by_path('Unknown_Linked_Page/') + response = self.get_by_path("Unknown_Linked_Page/") self.assertRedirects( - response, - resolve_url('wiki:create', path='') + '?slug=unknown_linked_page' + response, resolve_url("wiki:create", path="") + "?slug=unknown_linked_page" ) def test_article_list_update(self): @@ -84,244 +90,213 @@ class ArticleViewViewTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoC """ root_data = { - 'content': '[article_list depth:2]', - 'current_revision': str(URLPath.root().article.current_revision.id), - 'preview': '1', - 'title': 'Root Article' + "content": "[article_list depth:2]", + "current_revision": str(URLPath.root().article.current_revision.id), + "preview": "1", + "title": "Root Article", } - response = self.client.post(resolve_url('wiki:edit', path=''), root_data) - self.assertRedirects(response, resolve_url('wiki:root')) + response = self.client.post(resolve_url("wiki:edit", path=""), root_data) + self.assertRedirects(response, resolve_url("wiki:root")) # verify the new article is added to article_list response = self.client.post( - resolve_url('wiki:create', path=''), - {'title': 'Sub Article 1', 'slug': 'SubArticle1'} + resolve_url("wiki:create", path=""), + {"title": "Sub Article 1", "slug": "SubArticle1"}, ) - self.assertRedirects( - response, - resolve_url('wiki:get', path='subarticle1/') - ) - self.assertContains(self.get_by_path(''), 'Sub Article 1') - self.assertContains(self.get_by_path(''), 'subarticle1/') + self.assertRedirects(response, resolve_url("wiki:get", path="subarticle1/")) + self.assertContains(self.get_by_path(""), "Sub Article 1") + self.assertContains(self.get_by_path(""), "subarticle1/") # verify the deleted article is removed from article_list response = self.client.post( - resolve_url('wiki:delete', path='SubArticle1/'), - {'confirm': 'on', - 'purge': 'on', - 'revision': str(URLPath.objects.get(slug='subarticle1').article.current_revision.id), - } + resolve_url("wiki:delete", path="SubArticle1/"), + { + "confirm": "on", + "purge": "on", + "revision": str( + URLPath.objects.get(slug="subarticle1").article.current_revision.id + ), + }, ) - message = getattr(self.client.cookies['messages'], 'value') + message = getattr(self.client.cookies["messages"], "value") - self.assertRedirects( - response, - resolve_url('wiki:get', path='') - ) + self.assertRedirects(response, resolve_url("wiki:get", path="")) self.assertIn( - 'This article together with all ' - 'its contents are now completely gone', - message) - self.assertNotContains(self.get_by_path(''), 'Sub Article 1') - + "This article together with all " "its contents are now completely gone", + message, + ) + self.assertNotContains(self.get_by_path(""), "Sub Article 1") -class CreateViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): +class CreateViewTest( + RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase +): def test_create_nested_article_in_article(self): response = self.client.post( - resolve_url('wiki:create', path=''), - {'title': 'Level 1', 'slug': 'Level1', 'content': 'Content level 1'} - ) - self.assertRedirects( - response, - resolve_url('wiki:get', path='level1/') + resolve_url("wiki:create", path=""), + {"title": "Level 1", "slug": "Level1", "content": "Content level 1"}, ) + self.assertRedirects(response, resolve_url("wiki:get", path="level1/")) response = self.client.post( - resolve_url('wiki:create', path='Level1/'), - {'title': 'test', 'slug': 'Test', 'content': 'Content on level 2'} - ) - self.assertRedirects( - response, - resolve_url('wiki:get', path='level1/test/') + resolve_url("wiki:create", path="Level1/"), + {"title": "test", "slug": "Test", "content": "Content on level 2"}, ) + self.assertRedirects(response, resolve_url("wiki:get", path="level1/test/")) response = self.client.post( - resolve_url('wiki:create', path=''), - {'title': 'test', - 'slug': 'Test', - 'content': 'Other content on level 1' - } + resolve_url("wiki:create", path=""), + {"title": "test", "slug": "Test", "content": "Other content on level 1"}, ) - self.assertRedirects( - response, - resolve_url('wiki:get', path='test/') - ) - self.assertContains( - self.get_by_path('Test/'), - 'Other content on level 1' - ) - self.assertContains( - self.get_by_path('Level1/Test/'), - 'Content' - ) # on level 2') + self.assertRedirects(response, resolve_url("wiki:get", path="test/")) + self.assertContains(self.get_by_path("Test/"), "Other content on level 1") + self.assertContains(self.get_by_path("Level1/Test/"), "Content") # on level 2') def test_illegal_slug(self): # A slug cannot be '123' because it gets confused with an article ID. response = self.client.post( - resolve_url('wiki:create', path=''), - {'title': 'Illegal slug', 'slug': '123', 'content': 'blah'} - ) - self.assertContains( - response, - escape(validate_slug_numbers.message) + resolve_url("wiki:create", path=""), + {"title": "Illegal slug", "slug": "123", "content": "blah"}, ) + self.assertContains(response, escape(validate_slug_numbers.message)) class MoveViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): - def test_illegal_slug(self): # A slug cannot be '123' because it gets confused with an article ID. response = self.client.post( - resolve_url('wiki:move', path=''), - {'destination': '', 'slug': '123', 'redirect': ''} - ) - self.assertContains( - response, - escape(validate_slug_numbers.message) + resolve_url("wiki:move", path=""), + {"destination": "", "slug": "123", "redirect": ""}, ) + self.assertContains(response, escape(validate_slug_numbers.message)) def test_move(self): # Create a hierarchy of pages self.client.post( - resolve_url('wiki:create', path=''), - {'title': 'Test', 'slug': 'test0', 'content': 'Content .0.'} + resolve_url("wiki:create", path=""), + {"title": "Test", "slug": "test0", "content": "Content .0."}, ) self.client.post( - resolve_url('wiki:create', path='test0/'), - {'title': 'Test00', 'slug': 'test00', 'content': 'Content .00.'} + resolve_url("wiki:create", path="test0/"), + {"title": "Test00", "slug": "test00", "content": "Content .00."}, ) self.client.post( - resolve_url('wiki:create', path=''), - {'title': 'Test1', 'slug': 'test1', 'content': 'Content .1.'} + resolve_url("wiki:create", path=""), + {"title": "Test1", "slug": "test1", "content": "Content .1."}, ) self.client.post( - resolve_url('wiki:create', path='test1/'), - {'title': 'Tes10', 'slug': 'test10', 'content': 'Content .10.'} + resolve_url("wiki:create", path="test1/"), + {"title": "Tes10", "slug": "test10", "content": "Content .10."}, ) self.client.post( - resolve_url('wiki:create', path='test1/test10/'), - {'title': 'Test100', 'slug': 'test100', 'content': 'Content .100.'} + resolve_url("wiki:create", path="test1/test10/"), + {"title": "Test100", "slug": "test100", "content": "Content .100."}, ) # Move /test1 => /test0 (an already existing destination slug!) response = self.client.post( - resolve_url('wiki:move', path='test1/'), + resolve_url("wiki:move", path="test1/"), { - 'destination': str(URLPath.root().article.current_revision.id), - 'slug': 'test0', - 'redirect': '' - } + "destination": str(URLPath.root().article.current_revision.id), + "slug": "test0", + "redirect": "", + }, ) - self.assertContains(response, 'A slug named') - self.assertContains(response, 'already exists.') + self.assertContains(response, "A slug named") + self.assertContains(response, "already exists.") # Move /test1 >= /test2 (valid slug), no redirect - test0_id = URLPath.objects.get(slug='test0').article.current_revision.id + test0_id = URLPath.objects.get(slug="test0").article.current_revision.id response = self.client.post( - resolve_url('wiki:move', path='test1/'), - {'destination': str(test0_id), 'slug': 'test2', 'redirect': ''} - ) - self.assertRedirects( - response, - resolve_url('wiki:get', path='test0/test2/') + resolve_url("wiki:move", path="test1/"), + {"destination": str(test0_id), "slug": "test2", "redirect": ""}, ) + self.assertRedirects(response, resolve_url("wiki:get", path="test0/test2/")) # Check that there is no article displayed in this path anymore - response = self.get_by_path('test1/') - self.assertRedirects(response, '/_create/?slug=test1') + response = self.get_by_path("test1/") + self.assertRedirects(response, "/_create/?slug=test1") # Create /test0/test2/test020 response = self.client.post( - resolve_url('wiki:create', path='test0/test2/'), - {'title': 'Test020', 'slug': 'test020', 'content': 'Content .020.'} + resolve_url("wiki:create", path="test0/test2/"), + {"title": "Test020", "slug": "test020", "content": "Content .020."}, ) # Move /test0/test2 => /test1new + create redirect response = self.client.post( - resolve_url('wiki:move', path='test0/test2/'), + resolve_url("wiki:move", path="test0/test2/"), { - 'destination': str(URLPath.root().article.current_revision.id), - 'slug': 'test1new', 'redirect': 'true' - } - ) - self.assertRedirects( - response, - resolve_url('wiki:get', path='test1new/') + "destination": str(URLPath.root().article.current_revision.id), + "slug": "test1new", + "redirect": "true", + }, ) + self.assertRedirects(response, resolve_url("wiki:get", path="test1new/")) # Check that /test1new is a valid path - response = self.get_by_path('test1new/') - self.assertContains(response, 'Content .1.') + response = self.get_by_path("test1new/") + self.assertContains(response, "Content .1.") # Check that the child article test0/test2/test020 was also moved - response = self.get_by_path('test1new/test020/') - self.assertContains(response, 'Content .020.') + response = self.get_by_path("test1new/test020/") + self.assertContains(response, "Content .020.") - response = self.get_by_path('test0/test2/') - self.assertContains(response, 'Moved: Test1') - self.assertRegex(response.rendered_content, r'moved to <a[^>]*>wiki:/test1new/') + response = self.get_by_path("test0/test2/") + self.assertContains(response, "Moved: Test1") + self.assertRegex(response.rendered_content, r"moved to <a[^>]*>wiki:/test1new/") - response = self.get_by_path('test0/test2/test020/') - self.assertContains(response, 'Moved: Test020') - self.assertRegex(response.rendered_content, r'moved to <a[^>]*>wiki:/test1new/test020') + response = self.get_by_path("test0/test2/test020/") + self.assertContains(response, "Moved: Test020") + self.assertRegex( + response.rendered_content, r"moved to <a[^>]*>wiki:/test1new/test020" + ) # Check that moved_to was correctly set - urlsrc = URLPath.get_by_path('/test0/test2/') - urldst = URLPath.get_by_path('/test1new/') + urlsrc = URLPath.get_by_path("/test0/test2/") + urldst = URLPath.get_by_path("/test1new/") self.assertEqual(urlsrc.moved_to, urldst) # Check that moved_to was correctly set on the child's previous path - urlsrc = URLPath.get_by_path('/test0/test2/test020/') - urldst = URLPath.get_by_path('/test1new/test020/') + urlsrc = URLPath.get_by_path("/test0/test2/test020/") + urldst = URLPath.get_by_path("/test1new/test020/") self.assertEqual(urlsrc.moved_to, urldst) def test_translation(self): # Test that translation of "Be careful, links to this article" exists. self.client.post( - resolve_url('wiki:create', path=''), - {'title': 'Test', 'slug': 'test0', 'content': 'Content'} + resolve_url("wiki:create", path=""), + {"title": "Test", "slug": "test0", "content": "Content"}, ) - url = resolve_url('wiki:move', path='test0/') + url = resolve_url("wiki:move", path="test0/") response_en = self.client.get(url) - self.assertIn('Move article', response_en.rendered_content) - self.assertIn('Be careful', response_en.rendered_content) + self.assertIn("Move article", response_en.rendered_content) + self.assertIn("Be careful", response_en.rendered_content) - with translation.override('da-DK'): + with translation.override("da-DK"): response_da = self.client.get(url) - self.assertNotIn('Move article', response_da.rendered_content) - self.assertNotIn('Be careful', response_da.rendered_content) - + self.assertNotIn("Move article", response_da.rendered_content) + self.assertNotIn("Be careful", response_da.rendered_content) -class DeleteViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): +class DeleteViewTest( + RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase +): def test_render_delete_view(self): """ Other tests do not render the delete view but just sends a POST """ self.client.post( - resolve_url('wiki:create', path=''), - {'title': 'Test delete', 'slug': 'testdelete', 'content': 'To be deleted'} - ) - response = self.client.get( - resolve_url('wiki:delete', path='testdelete/'), + resolve_url("wiki:create", path=""), + {"title": "Test delete", "slug": "testdelete", "content": "To be deleted"}, ) + response = self.client.get(resolve_url("wiki:delete", path="testdelete/"),) # test the cache - self.assertContains(response, 'Delete article') + self.assertContains(response, "Delete article") def test_articles_cache_is_cleared_after_deleting(self): @@ -329,35 +304,31 @@ class DeleteViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientT # revealed only by sequence of tests in some particular order response = self.client.post( - resolve_url('wiki:create', path=''), - {'title': 'Test cache', 'slug': 'testcache', 'content': 'Content 1'} + resolve_url("wiki:create", path=""), + {"title": "Test cache", "slug": "testcache", "content": "Content 1"}, ) - self.assertRedirects( - response, - resolve_url('wiki:get', path='testcache/') - ) + self.assertRedirects(response, resolve_url("wiki:get", path="testcache/")) response = self.client.post( - resolve_url('wiki:delete', path='testcache/'), - {'confirm': 'on', 'purge': 'on', - 'revision': str(URLPath.objects.get(slug='testcache').article.current_revision.id)} + resolve_url("wiki:delete", path="testcache/"), + { + "confirm": "on", + "purge": "on", + "revision": str( + URLPath.objects.get(slug="testcache").article.current_revision.id + ), + }, ) - self.assertRedirects( - response, - resolve_url('wiki:get', path='') - ) + self.assertRedirects(response, resolve_url("wiki:get", path="")) response = self.client.post( - resolve_url('wiki:create', path=''), - {'title': 'Test cache', 'slug': 'TestCache', 'content': 'Content 2'} + resolve_url("wiki:create", path=""), + {"title": "Test cache", "slug": "TestCache", "content": "Content 2"}, ) - self.assertRedirects( - response, - resolve_url('wiki:get', path='testcache/') - ) - self.assertContains(self.get_by_path('TestCache/'), 'Content 2') + self.assertRedirects(response, resolve_url("wiki:get", path="testcache/")) + self.assertContains(self.get_by_path("TestCache/"), "Content 2") def test_deleted_view(self): """ @@ -366,33 +337,41 @@ class DeleteViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientT """ # 1. Create the article self.client.post( - resolve_url('wiki:create', path=''), - {'title': 'Test delete', 'slug': 'testdelete', 'content': 'To be deleted'} + resolve_url("wiki:create", path=""), + {"title": "Test delete", "slug": "testdelete", "content": "To be deleted"}, ) # 2. Soft delete it self.client.post( - resolve_url('wiki:delete', path='testdelete/'), - {'confirm': 'on', 'purge': '', - 'revision': str(URLPath.objects.get(slug='testdelete').article.current_revision.id)} + resolve_url("wiki:delete", path="testdelete/"), + { + "confirm": "on", + "purge": "", + "revision": str( + URLPath.objects.get(slug="testdelete").article.current_revision.id + ), + }, ) # 3. Get and test that it redirects to the deleted page response = self.client.get( - resolve_url('wiki:get', path='testdelete/'), - follow=True, + resolve_url("wiki:get", path="testdelete/"), follow=True, ) # test that it's the Deleted page - self.assertContains(response, 'Article deleted') + self.assertContains(response, "Article deleted") # 4. Test that we can purge the page now self.client.post( - resolve_url('wiki:deleted', path='testdelete/'), - {'confirm': 'on', 'purge': 'on', - 'revision': str(URLPath.objects.get(slug='testdelete').article.current_revision.id)} + resolve_url("wiki:deleted", path="testdelete/"), + { + "confirm": "on", + "purge": "on", + "revision": str( + URLPath.objects.get(slug="testdelete").article.current_revision.id + ), + }, ) # 5. Test that it's not found anymore response = self.client.get( - resolve_url('wiki:get', path='testdelete/'), - follow=True, + resolve_url("wiki:get", path="testdelete/"), follow=True, ) self.assertContains(response, "Add new article") @@ -408,44 +387,39 @@ class DeleteViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientT class EditViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): - def test_preview_save(self): """Test edit preview, edit save and messages.""" example_data = { - 'content': 'The modified text', - 'current_revision': str(URLPath.root().article.current_revision.id), - 'preview': '1', + "content": "The modified text", + "current_revision": str(URLPath.root().article.current_revision.id), + "preview": "1", # 'save': '1', # probably not too important - 'summary': 'why edited', - 'title': 'wiki test' + "summary": "why edited", + "title": "wiki test", } # test preview response = self.client.post( - resolve_url('wiki:preview', path=''), # url: '/_preview/' - example_data + resolve_url("wiki:preview", path=""), example_data # url: '/_preview/' ) - self.assertContains(response, 'The modified text') + self.assertContains(response, "The modified text") def test_preview_xframe_options_sameorigin(self): """Ensure that preview response has X-Frame-Options: SAMEORIGIN""" example_data = { - 'content': 'The modified text', - 'current_revision': str(URLPath.root().article.current_revision.id), - 'preview': '1', - 'summary': 'why edited', - 'title': 'wiki test' + "content": "The modified text", + "current_revision": str(URLPath.root().article.current_revision.id), + "preview": "1", + "summary": "why edited", + "title": "wiki test", } - response = self.client.post( - resolve_url('wiki:preview', path=''), - example_data - ) + response = self.client.post(resolve_url("wiki:preview", path=""), example_data) - self.assertEquals(response.get('X-Frame-Options'), 'SAMEORIGIN') + self.assertEquals(response.get("X-Frame-Options"), "SAMEORIGIN") def test_revision_conflict(self): """ @@ -453,48 +427,40 @@ class EditViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTes """ example_data = { - 'content': 'More modifications', - 'current_revision': str(URLPath.root().article.current_revision.id), - 'preview': '0', - 'save': '1', - 'summary': 'why edited', - 'title': 'wiki test' + "content": "More modifications", + "current_revision": str(URLPath.root().article.current_revision.id), + "preview": "0", + "save": "1", + "summary": "why edited", + "title": "wiki test", } - response = self.client.post( - resolve_url('wiki:edit', path=''), - example_data - ) + response = self.client.post(resolve_url("wiki:edit", path=""), example_data) - self.assertRedirects(response, resolve_url('wiki:root')) + self.assertRedirects(response, resolve_url("wiki:root")) - response = self.client.post( - resolve_url('wiki:edit', path=''), - example_data - ) + response = self.client.post(resolve_url("wiki:edit", path=""), example_data) self.assertContains( - response, - 'While you were editing, someone else changed the revision.' + response, "While you were editing, someone else changed the revision." ) class DiffViewTests(RequireRootArticleMixin, DjangoClientTestBase): - def setUp(self): super().setUp() - self.root_article.add_revision(ArticleRevision( - title='New Revision'), save=True - ) + self.root_article.add_revision(ArticleRevision(title="New Revision"), save=True) self.new_revision = self.root_article.current_revision def test_diff(self): - response = self.client.get(reverse('wiki:diff', kwargs={'revision_id': self.root_article.pk})) + response = self.client.get( + reverse("wiki:diff", kwargs={"revision_id": self.root_article.pk}) + ) diff = { "diff": ["+ root article content"], - "other_changes": [["New title", "Root Article"]] + "other_changes": [["New title", "Root Article"]], } - self.assertJSONEqual(str(response.content, encoding='utf8'), diff) + self.assertJSONEqual(str(response.content, encoding="utf8"), diff) self.assertIsInstance(response, JsonResponse) self.assertEqual(response.status_code, 200) @@ -502,17 +468,19 @@ class DiffViewTests(RequireRootArticleMixin, DjangoClientTestBase): class EditViewTestsBase(RequireRootArticleMixin, FuncBaseMixin): def test_edit_save(self): old_revision = URLPath.root().article.current_revision - self.get_url('wiki:edit', path='') - self.fill({ - '#id_content': 'Something 2', - '#id_summary': 'why edited', - '#id_title': 'wiki test' - }) - self.submit('#id_save') - self.assertTextPresent('Something 2') - self.assertTextPresent('successfully added') + self.get_url("wiki:edit", path="") + self.fill( + { + "#id_content": "Something 2", + "#id_summary": "why edited", + "#id_title": "wiki test", + } + ) + self.submit("#id_save") + self.assertTextPresent("Something 2") + self.assertTextPresent("successfully added") new_revision = URLPath.root().article.current_revision - self.assertIn('Something 2', new_revision.content) + self.assertIn("Something 2", new_revision.content) self.assertEqual(new_revision.revision_number, old_revision.revision_number + 1) @@ -524,115 +492,116 @@ class EditViewTestsSelenium(EditViewTestsBase, SeleniumBase): # Javascript only tests: def test_preview_and_save(self): - self.get_url('wiki:edit', path='') - self.fill({ - '#id_content': 'Some changed stuff', - '#id_summary': 'why edited', - '#id_title': 'wiki test' - }) - self.click('#id_preview') - self.submit('#id_preview_save_changes') + self.get_url("wiki:edit", path="") + self.fill( + { + "#id_content": "Some changed stuff", + "#id_summary": "why edited", + "#id_title": "wiki test", + } + ) + self.click("#id_preview") + self.submit("#id_preview_save_changes") new_revision = URLPath.root().article.current_revision self.assertIn("Some changed stuff", new_revision.content) -class SearchViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): - +class SearchViewTest( + RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase +): def test_query_string(self): - response = self.client.get(resolve_url('wiki:search'), {'q': 'Article'}) - self.assertContains(response, 'Root Article') + response = self.client.get(resolve_url("wiki:search"), {"q": "Article"}) + self.assertContains(response, "Root Article") def test_empty_query_string(self): - response = self.client.get(resolve_url('wiki:search'), {'q': ''}) - self.assertFalse(response.context['articles']) + response = self.client.get(resolve_url("wiki:search"), {"q": ""}) + self.assertFalse(response.context["articles"]) def test_hierarchy_search(self): c = self.client c.post( - resolve_url('wiki:create', path=''), - {'title': 'Test0', 'slug': 'test0', 'content': 'Content test0'} + resolve_url("wiki:create", path=""), + {"title": "Test0", "slug": "test0", "content": "Content test0"}, ) c.post( - resolve_url('wiki:create', path=''), - {'title': 'Test1', 'slug': 'test1', 'content': 'Content test1'} + resolve_url("wiki:create", path=""), + {"title": "Test1", "slug": "test1", "content": "Content test1"}, ) c.post( - resolve_url('wiki:create', path='test0/'), - {'title': 'Subtest0', 'slug': 'subtest0', 'content': 'Content test2'} + resolve_url("wiki:create", path="test0/"), + {"title": "Subtest0", "slug": "subtest0", "content": "Content test2"}, ) - response = c.get(resolve_url('wiki:search', path='test0/'), {'q': 'Content test'}) - articles = response.context['articles'] + response = c.get( + resolve_url("wiki:search", path="test0/"), {"q": "Content test"} + ) + articles = response.context["articles"] def contains_title(articles, title): return any(article.current_revision.title == title for article in articles) - self.assertIs(contains_title(articles, 'Test0'), True) - self.assertIs(contains_title(articles, 'Test1'), False) - self.assertIs(contains_title(articles, 'Subtest0'), True) + self.assertIs(contains_title(articles, "Test0"), True) + self.assertIs(contains_title(articles, "Test1"), False) + self.assertIs(contains_title(articles, "Subtest0"), True) def test_hierarchy_search_404(self): c = self.client - response = c.get(resolve_url( - 'wiki:search', path='test0/'), {'q': 'Content test'}) + response = c.get( + resolve_url("wiki:search", path="test0/"), {"q": "Content test"} + ) self.assertEqual(response.status_code, 404) -class DeletedListViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): - +class DeletedListViewTest( + RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase +): def test_deleted_articles_list(self): response = self.client.post( - resolve_url('wiki:create', path=''), - {'title': 'Delete Me', 'slug': 'deleteme', 'content': 'delete me please!'} + resolve_url("wiki:create", path=""), + {"title": "Delete Me", "slug": "deleteme", "content": "delete me please!"}, ) - self.assertRedirects( - response, - resolve_url('wiki:get', path='deleteme/') - ) + self.assertRedirects(response, resolve_url("wiki:get", path="deleteme/")) response = self.client.post( - resolve_url('wiki:delete', path='deleteme/'), - {'confirm': 'on', - 'revision': URLPath.objects.get(slug='deleteme').article.current_revision.id} + resolve_url("wiki:delete", path="deleteme/"), + { + "confirm": "on", + "revision": URLPath.objects.get( + slug="deleteme" + ).article.current_revision.id, + }, ) - self.assertRedirects( - response, - resolve_url('wiki:get', path='') - ) + self.assertRedirects(response, resolve_url("wiki:get", path="")) - response = self.client.get(resolve_url('wiki:deleted_list')) - self.assertContains(response, 'Delete Me') + response = self.client.get(resolve_url("wiki:deleted_list")) + self.assertContains(response, "Delete Me") class MergeViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): - def test_merge_preview(self): """Test merge preview""" first_revision = self.root_article.current_revision example_data = { - 'content': 'More modifications\n\nMerge new line', - 'current_revision': str(first_revision.id), - 'preview': '0', - 'save': '1', - 'summary': 'testing merge', - 'title': 'wiki test' + "content": "More modifications\n\nMerge new line", + "current_revision": str(first_revision.id), + "preview": "0", + "save": "1", + "summary": "testing merge", + "title": "wiki test", } # save a new revision - self.client.post( - resolve_url('wiki:edit', path=''), - example_data - ) + self.client.post(resolve_url("wiki:edit", path=""), example_data) new_revision = models.Article.objects.get( id=self.root_article.id @@ -640,172 +609,163 @@ class MergeViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTe response = self.client.get( resolve_url( - 'wiki:merge_revision_preview', - article_id=self.root_article.id, revision_id=first_revision.id + "wiki:merge_revision_preview", + article_id=self.root_article.id, + revision_id=first_revision.id, ), ) + self.assertContains(response, "Previewing merge between:") self.assertContains( - response, - 'Previewing merge between:' - ) - self.assertContains( - response, - '#{rev_number}'.format(rev_number=first_revision.revision_number) + response, "#{rev_number}".format(rev_number=first_revision.revision_number) ) self.assertContains( - response, - '#{rev_number}'.format(rev_number=new_revision.revision_number) + response, "#{rev_number}".format(rev_number=new_revision.revision_number) ) -class SourceViewTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): +class SourceViewTests( + RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase +): def test_template_used(self): - response = self.client.get(reverse('wiki:source', kwargs={ - 'article_id': self.root_article.pk, - })) + response = self.client.get( + reverse("wiki:source", kwargs={"article_id": self.root_article.pk,}) + ) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, template_name='wiki/source.html') + self.assertTemplateUsed(response, template_name="wiki/source.html") def test_can_read_permission(self): # everybody can see the source of an article self.client.logout() - response = self.client.get(reverse('wiki:source', kwargs={ - 'article_id': self.root_article.pk, - })) + response = self.client.get( + reverse("wiki:source", kwargs={"article_id": self.root_article.pk,}) + ) self.assertEqual(response.status_code, 200) def test_content(self): - response = self.client.get(reverse('wiki:source', kwargs={ - 'article_id': self.root_article.pk, - })) - self.assertIn('Source of ', str(response.content)) - self.assertEqual(response.context['selected_tab'], 'source') - + response = self.client.get( + reverse("wiki:source", kwargs={"article_id": self.root_article.pk,}) + ) + self.assertIn("Source of ", str(response.content)) + self.assertEqual(response.context["selected_tab"], "source") -class HistoryViewTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): +class HistoryViewTests( + RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase +): def test_can_read_permission(self): - response = self.client.get(reverse('wiki:history', kwargs={ - 'article_id': self.root_article.pk, - })) + response = self.client.get( + reverse("wiki:history", kwargs={"article_id": self.root_article.pk,}) + ) self.assertEqual(response.status_code, 200) def test_content(self): - response = self.client.get(reverse('wiki:history', kwargs={ - 'article_id': self.root_article.pk, - })) - self.assertContains(response, 'History:') - self.assertEqual(response.context['selected_tab'], 'history') + response = self.client.get( + reverse("wiki:history", kwargs={"article_id": self.root_article.pk,}) + ) + self.assertContains(response, "History:") + self.assertEqual(response.context["selected_tab"], "history") class DirViewTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): - def test_browse_root(self): - response = self.client.get( - reverse('wiki:dir', kwargs={'path': ''}), - ) + response = self.client.get(reverse("wiki:dir", kwargs={"path": ""}),) self.assertRegex( - response.rendered_content, - r'Browsing\s+<strong><a href=".+">/</a></strong>' + response.rendered_content, r'Browsing\s+<strong><a href=".+">/</a></strong>' ) def test_browse_root_query(self): self.client.post( - resolve_url('wiki:create', path=''), - {'title': 'Test', 'slug': 'test0', 'content': 'Content .0.'} + resolve_url("wiki:create", path=""), + {"title": "Test", "slug": "test0", "content": "Content .0."}, ) self.client.post( - resolve_url('wiki:create', path='test0/'), - {'title': 'Test00', 'slug': 'test00', 'content': 'Content .00.'} + resolve_url("wiki:create", path="test0/"), + {"title": "Test00", "slug": "test00", "content": "Content .00."}, ) response = self.client.get( - reverse('wiki:dir', kwargs={'path': ''}), - {'query': "Test"}, - ) - self.assertRegex( - response.rendered_content, - r'1 article' + reverse("wiki:dir", kwargs={"path": ""}), {"query": "Test"}, ) + self.assertRegex(response.rendered_content, r"1 article") response = self.client.get( - reverse('wiki:dir', kwargs={'path': 'test0/'}), - {'query': "Test00"}, - ) - self.assertRegex( - response.rendered_content, - r'1 article' + reverse("wiki:dir", kwargs={"path": "test0/"}), {"query": "Test00"}, ) + self.assertRegex(response.rendered_content, r"1 article") -class SettingsViewTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): - +class SettingsViewTests( + RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase +): def test_change_group(self): group = CustomGroup.objects.create() response = self.client.post( - resolve_url('wiki:settings', article_id=self.root_article.pk) + "?f=form0", - { - 'group': group.pk, - 'owner_username': SUPERUSER1_USERNAME, - }, - follow=True + resolve_url("wiki:settings", article_id=self.root_article.pk) + "?f=form0", + {"group": group.pk, "owner_username": SUPERUSER1_USERNAME,}, + follow=True, ) self.root_article.refresh_from_db() self.assertEqual(self.root_article.group, group) self.assertEqual(self.root_article.owner, self.superuser1) - self.assertEqual(len(response.context.get('messages')), 1) - message = response.context.get('messages')._loaded_messages[0] + self.assertEqual(len(response.context.get("messages")), 1) + message = response.context.get("messages")._loaded_messages[0] self.assertEqual(message.level, constants.SUCCESS) - self.assertEqual(message.message, 'Permission settings for the article were updated.') + self.assertEqual( + message.message, "Permission settings for the article were updated." + ) def test_change_invalid_owner(self): self.assertIsNone(self.root_article.owner) - response = self.client.post(resolve_url('wiki:settings', article_id=self.root_article.pk) + "?f=form0", { - 'owner_username': 'invalid' - }, follow=True) - self.assertEqual(response.context['forms'][0].errors['owner_username'], ['No user with that username']) + response = self.client.post( + resolve_url("wiki:settings", article_id=self.root_article.pk) + "?f=form0", + {"owner_username": "invalid"}, + follow=True, + ) + self.assertEqual( + response.context["forms"][0].errors["owner_username"], + ["No user with that username"], + ) def test_unchanged_message(self): # 1. This is not pretty: Constructs a request object to use to construct # the PermissionForm get_response = self.client.get( - resolve_url( - 'wiki:settings', - article_id=self.root_article.pk - ) + resolve_url("wiki:settings", article_id=self.root_article.pk) ) # 2. Construct a PermissionForm form = PermissionsForm(self.root_article, get_response.wsgi_request) # 3. ...in order to get the POST form values that will be transmitted - form_values = { - field.html_name: field.value() or "" for field in form - } + form_values = {field.html_name: field.value() or "" for field in form} # 4. Send an unchanged form response = self.client.post( - resolve_url( - 'wiki:settings', - article_id=self.root_article.pk - ) + "?f=form0", + resolve_url("wiki:settings", article_id=self.root_article.pk) + "?f=form0", form_values, - follow=True + follow=True, ) - self.assertEqual(len(response.context.get('messages')), 1) - message = response.context.get('messages')._loaded_messages[0] + self.assertEqual(len(response.context.get("messages")), 1) + message = response.context.get("messages")._loaded_messages[0] self.assertEqual(message.level, constants.SUCCESS) - self.assertEqual(message.message, 'Your permission settings were unchanged, so nothing saved.') + self.assertEqual( + message.message, + "Your permission settings were unchanged, so nothing saved.", + ) @override_settings(ACCOUNT_HANDLING=True) def test_login_required(self): self.client.logout() - response = self.client.get(reverse('wiki:settings', kwargs={'article_id': self.root_article.pk})) + response = self.client.get( + reverse("wiki:settings", kwargs={"article_id": self.root_article.pk}) + ) # it's redirecting self.assertEqual(response.status_code, 302) def test_auth_user(self): - response = self.client.get(reverse('wiki:settings', kwargs={'article_id': self.root_article.pk})) + response = self.client.get( + reverse("wiki:settings", kwargs={"article_id": self.root_article.pk}) + ) self.assertEqual(response.status_code, 200) def test_content(self): - response = self.client.get(reverse('wiki:settings', kwargs={ - 'article_id': self.root_article.pk, - })) - self.assertEqual(response.context['selected_tab'], 'settings') + response = self.client.get( + reverse("wiki:settings", kwargs={"article_id": self.root_article.pk,}) + ) + self.assertEqual(response.context["selected_tab"], "settings") diff --git a/tests/plugins/attachments/test_commands.py b/tests/plugins/attachments/test_commands.py index 590c550e..5b40345c 100644 --- a/tests/plugins/attachments/test_commands.py +++ b/tests/plugins/attachments/test_commands.py @@ -14,18 +14,15 @@ class TestAttachmentManagementCommands(TestManagementCommands): def setUp(self): super().setUp() - self.test_file = tempfile.NamedTemporaryFile('w', delete=False, suffix=".txt") + self.test_file = tempfile.NamedTemporaryFile("w", delete=False, suffix=".txt") self.test_file.write("test") - self.child1 = URLPath.create_urlpath(self.root, 'test-slug', title="Test 1") + self.child1 = URLPath.create_urlpath(self.root, "test-slug", title="Test 1") - self.attachment1 = models.Attachment.objects.create( - article=self.child1.article - ) + self.attachment1 = models.Attachment.objects.create(article=self.child1.article) self.attachment1_revision1 = models.AttachmentRevision.objects.create( - attachment=self.attachment1, - file=self.test_file.name, + attachment=self.attachment1, file=self.test_file.name, ) def tearDown(self): diff --git a/tests/plugins/attachments/test_models.py b/tests/plugins/attachments/test_models.py index 9441a38f..b48a2144 100644 --- a/tests/plugins/attachments/test_models.py +++ b/tests/plugins/attachments/test_models.py @@ -3,15 +3,13 @@ from wiki.plugins.attachments.models import Attachment, AttachmentRevision class AttachmentRevisionTests(RequireRootArticleMixin, TestBase): - def setUp(self): super().setUp() self.attachment = Attachment.objects.create( - article=self.root_article, original_filename='blah.txt', + article=self.root_article, original_filename="blah.txt", ) self.revision = AttachmentRevision.objects.create( - attachment=self.attachment, file=None, description='muh', - revision_number=1, + attachment=self.attachment, file=None, description="muh", revision_number=1, ) def test_revision_no_file(self): @@ -26,6 +24,6 @@ class AttachmentRevisionTests(RequireRootArticleMixin, TestBase): self.assertIsNone(self.revision.get_filename()) def test_str(self): - self.assertEqual(str(self.revision), "%s: %s (r%d)" % ( - 'Root Article', 'blah.txt', 1, - )) + self.assertEqual( + str(self.revision), "%s: %s (r%d)" % ("Root Article", "blah.txt", 1,) + ) diff --git a/tests/plugins/attachments/test_views.py b/tests/plugins/attachments/test_views.py index 4efe9abf..97e1aa54 100644 --- a/tests/plugins/attachments/test_views.py +++ b/tests/plugins/attachments/test_views.py @@ -7,13 +7,14 @@ from wiki.models import URLPath from ...base import ArticleWebTestUtils, DjangoClientTestBase, RequireRootArticleMixin -class AttachmentTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): - +class AttachmentTests( + RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase +): def setUp(self): super().setUp() self.article = self.root_article self.test_data = "This is a plain text file" - self.test_description = 'My file' + self.test_description = "My file" def _createTxtFilestream(self, strData, **kwargs): """ @@ -25,29 +26,20 @@ class AttachmentTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClient Optional Arguments : filename : str, Defaults to 'test.txt' """ - filename = kwargs.get('filename', 'test.txt') - data = strData.encode('utf-8') + filename = kwargs.get("filename", "test.txt") + data = strData.encode("utf-8") filedata = BytesIO(data) filestream = InMemoryUploadedFile( - filedata, - None, - filename, - 'text', - len(data), - None + filedata, None, filename, "text", len(data), None ) return filestream def _create_test_attachment(self, path): - url = reverse('wiki:attachments_index', kwargs={'path': path}) + url = reverse("wiki:attachments_index", kwargs={"path": path}) filestream = self._createTxtFilestream(self.test_data) response = self.client.post( url, - { - 'description': self.test_description, - 'file': filestream, - 'save': '1', - } + {"description": self.test_description, "file": filestream, "save": "1",}, ) self.assertRedirects(response, url) @@ -57,11 +49,13 @@ class AttachmentTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClient Uploading a file should preserve the original filename. Uploading should not modify file in any way. """ - self._create_test_attachment('') + self._create_test_attachment("") # Check the object was created. attachment = self.article.shared_plugins_set.all()[0].attachment - self.assertEqual(attachment.original_filename, 'test.txt') - self.assertEqual(attachment.current_revision.file.file.read(), self.test_data.encode('utf-8')) + self.assertEqual(attachment.original_filename, "test.txt") + self.assertEqual( + attachment.current_revision.file.file.read(), self.test_data.encode("utf-8") + ) def test_replace(self): """ @@ -70,10 +64,12 @@ class AttachmentTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClient "replace" is checked. """ # Upload initial file - url = reverse('wiki:attachments_index', kwargs={'path': ''}) + url = reverse("wiki:attachments_index", kwargs={"path": ""}) data = "This is a plain text file" filestream = self._createTxtFilestream(data) - self.client.post(url, {'description': 'My file', 'file': filestream, 'save': '1', }) + self.client.post( + url, {"description": "My file", "file": filestream, "save": "1",} + ) attachment = self.article.shared_plugins_set.all()[0].attachment # uploading for the first time should mean that there is only one revision. @@ -81,45 +77,47 @@ class AttachmentTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClient # Change url to replacement page. url = reverse( - 'wiki:attachments_replace', - kwargs={'attachment_id': attachment.id, 'article_id': self.article.id} + "wiki:attachments_replace", + kwargs={"attachment_id": attachment.id, "article_id": self.article.id}, ) # Upload replacement without removing revisions - replacement_data = data + ' And this is my edit' + replacement_data = data + " And this is my edit" replacement_filestream = self._createTxtFilestream(replacement_data) self.client.post( - url, - { - 'description': 'Replacement upload', - 'file': replacement_filestream, - } + url, {"description": "Replacement upload", "file": replacement_filestream,} ) attachment = self.article.shared_plugins_set.all()[0].attachment # Revision count should be two self.assertEqual(attachment.attachmentrevision_set.count(), 2) # Original filenames should not be modified - self.assertEqual(attachment.original_filename, 'test.txt') + self.assertEqual(attachment.original_filename, "test.txt") # Latest revision should equal replacment_data - self.assertEqual(attachment.current_revision.file.file.read(), replacement_data.encode('utf-8')) + self.assertEqual( + attachment.current_revision.file.file.read(), + replacement_data.encode("utf-8"), + ) first_replacement = attachment.current_revision # Upload another replacement, this time removing most recent revision - replacement_data2 = data + ' And this is a different edit' + replacement_data2 = data + " And this is a different edit" replacement_filestream2 = self._createTxtFilestream(replacement_data2) self.client.post( url, { - 'description': 'Replacement upload', - 'file': replacement_filestream2, - 'replace': 'on', - } + "description": "Replacement upload", + "file": replacement_filestream2, + "replace": "on", + }, ) attachment = self.article.shared_plugins_set.all()[0].attachment # Revision count should still be two self.assertEqual(attachment.attachmentrevision_set.count(), 2) # Latest revision should equal replacment_data2 - self.assertEqual(attachment.current_revision.file.file.read(), replacement_data2.encode('utf-8')) + self.assertEqual( + attachment.current_revision.file.file.read(), + replacement_data2.encode("utf-8"), + ) # The first replacement should no longer be in the filehistory self.assertNotIn(first_replacement, attachment.attachmentrevision_set.all()) @@ -127,23 +125,20 @@ class AttachmentTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClient """ Call the search view """ - self._create_test_attachment('') - url = reverse('wiki:attachments_search', kwargs={'path': ''}) - response = self.client.get(url, {'query': self.test_description}) + self._create_test_attachment("") + url = reverse("wiki:attachments_search", kwargs={"path": ""}) + response = self.client.get(url, {"query": self.test_description}) self.assertContains(response, self.test_description) def get_article(self, cont): urlpath = URLPath.create_urlpath( - URLPath.root(), - "html_attach", - title="TestAttach", - content=cont + URLPath.root(), "html_attach", title="TestAttach", content=cont ) self._create_test_attachment(urlpath.path) return urlpath.article.render() def test_render(self): - output = self.get_article('[attachment:1]') + output = self.get_article("[attachment:1]") expected = ( '<span class="attachment"><a href=".*attachments/download/1/"' ' title="Click to download test\.txt">\s*test\.txt\s*</a>' @@ -151,10 +146,8 @@ class AttachmentTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClient self.assertRegexpMatches(output, expected) def test_render_missing(self): - output = self.get_article('[attachment:2]') - expected = ( - '<span class="attachment attachment-deleted">\s*Attachment with ID #2 is deleted.\s*</span>' - ) + output = self.get_article("[attachment:2]") + expected = '<span class="attachment attachment-deleted">\s*Attachment with ID #2 is deleted.\s*</span>' self.assertRegexpMatches(output, expected) def test_render_title(self): diff --git a/tests/plugins/editsection/test_editsection.py b/tests/plugins/editsection/test_editsection.py index bbe1e12f..c864362e 100644 --- a/tests/plugins/editsection/test_editsection.py +++ b/tests/plugins/editsection/test_editsection.py @@ -5,28 +5,29 @@ from wiki.models import URLPath from ...base import DjangoClientTestBase, RequireRootArticleMixin, WebTestBase TEST_CONTENT = ( - 'Title 1\n' - '=======\n' - '## Title 2\n' - 'Title 3\n' - '-------\n' - 'a\n' - 'Paragraph\n' - '-------\n' - '### Title 4\n' - '## Title 5\n' - '# Title 6\n' + "Title 1\n" + "=======\n" + "## Title 2\n" + "Title 3\n" + "-------\n" + "a\n" + "Paragraph\n" + "-------\n" + "### Title 4\n" + "## Title 5\n" + "# Title 6\n" ) class EditSectionTests(RequireRootArticleMixin, DjangoClientTestBase): def test_editsection(self): # Test creating links to allow editing all sections individually - urlpath = URLPath.create_urlpath(URLPath.root(), "testedit", - title="TestEdit", content=TEST_CONTENT) + urlpath = URLPath.create_urlpath( + URLPath.root(), "testedit", title="TestEdit", content=TEST_CONTENT + ) output = urlpath.article.render() expected = ( - r'(?s)' + r"(?s)" r'Title 1<a class="article-edit-title-link" href="/testedit/_plugin/editsection/1-0-0/header/T1/">\[edit\]</a>.*' r'Title 2<a class="article-edit-title-link" href="/testedit/_plugin/editsection/1-1-0/header/T2/">\[edit\]</a>.*' r'Title 3<a class="article-edit-title-link" href="/testedit/_plugin/editsection/1-2-0/header/T3/">\[edit\]</a>.*' @@ -37,32 +38,37 @@ class EditSectionTests(RequireRootArticleMixin, DjangoClientTestBase): self.assertRegex(output, expected) # Test wrong header text. Editing should fail with a redirect. - url = reverse('wiki:editsection', - kwargs={'path': 'testedit/', 'location': '1-2-1', 'header': 'Test'}) + url = reverse( + "wiki:editsection", + kwargs={"path": "testedit/", "location": "1-2-1", "header": "Test"}, + ) response = self.client.get(url) - self.assertRedirects(response, reverse('wiki:get', kwargs={'path': 'testedit/'})) + self.assertRedirects( + response, reverse("wiki:get", kwargs={"path": "testedit/"}) + ) # Test extracting sections for editing - url = reverse('wiki:editsection', - kwargs={'path': 'testedit/', 'location': '1-2-1', 'header': 'T4'}) - response = self.client.get(url) - expected = ( - '>### Title 4[\r\n]*' - '<' + url = reverse( + "wiki:editsection", + kwargs={"path": "testedit/", "location": "1-2-1", "header": "T4"}, ) + response = self.client.get(url) + expected = ">### Title 4[\r\n]*" "<" self.assertRegex(response.rendered_content, expected) - url = reverse('wiki:editsection', - kwargs={'path': 'testedit/', 'location': '1-2-0', 'header': 'T3'}) + url = reverse( + "wiki:editsection", + kwargs={"path": "testedit/", "location": "1-2-0", "header": "T3"}, + ) response = self.client.get(url) expected = ( - '>Title 3[\r\n]*' - '-------[\r\n]*' - 'a[\r\n]*' - 'Paragraph[\r\n]*' - '-------[\r\n]*' - '### Title 4[\r\n]*' - '<' + ">Title 3[\r\n]*" + "-------[\r\n]*" + "a[\r\n]*" + "Paragraph[\r\n]*" + "-------[\r\n]*" + "### Title 4[\r\n]*" + "<" ) self.assertRegex(response.rendered_content, expected) @@ -74,26 +80,31 @@ class EditSectionEditBase(RequireRootArticleMixin, FuncBaseMixin): class EditSectionEditTests(EditSectionEditBase, WebTestBase): # Test editing a section def test_editsection_edit(self): - urlpath = URLPath.create_urlpath(URLPath.root(), "testedit", - title="TestEdit", content=TEST_CONTENT) + urlpath = URLPath.create_urlpath( + URLPath.root(), "testedit", title="TestEdit", content=TEST_CONTENT + ) old_number = urlpath.article.current_revision.revision_number - self.get_literal_url(reverse('wiki:editsection', - kwargs={'path': 'testedit/', 'location': '1-2-0', 'header': 'T3'})) - self.fill({ - '#id_content': '# Header 1\nContent of the new section' - }) - self.submit('#id_save') + self.get_literal_url( + reverse( + "wiki:editsection", + kwargs={"path": "testedit/", "location": "1-2-0", "header": "T3"}, + ) + ) + self.fill({"#id_content": "# Header 1\nContent of the new section"}) + self.submit("#id_save") expected = ( - r'(?s)' + r"(?s)" r'Title 1<a class="article-edit-title-link" href="/testedit/_plugin/editsection/1-0-0/header/T1/">\[edit\]</a>.*' r'Title 2<a class="article-edit-title-link" href="/testedit/_plugin/editsection/1-1-0/header/T2/">\[edit\]</a>.*' r'Header 1<a class="article-edit-title-link" href="/testedit/_plugin/editsection/2-0-0/header/H1/">\[edit\]</a>.*' - r'Content of the new section.*' + r"Content of the new section.*" r'Title 5<a class="article-edit-title-link" href="/testedit/_plugin/editsection/2-1-0/header/T5/">\[edit\]</a>.*' r'Title 6<a class="article-edit-title-link" href="/testedit/_plugin/editsection/3-0-0/header/T6/">\[edit\]</a>.*' ) - self.assertRegex(self.last_response.content.decode('utf-8'), expected) + self.assertRegex(self.last_response.content.decode("utf-8"), expected) - new_number = URLPath.objects.get(slug='testedit').article.current_revision.revision_number + new_number = URLPath.objects.get( + slug="testedit" + ).article.current_revision.revision_number self.assertEqual(new_number, old_number + 1) diff --git a/tests/plugins/globalhistory/test_globalhistory.py b/tests/plugins/globalhistory/test_globalhistory.py index 508837ef..2d2cc0a1 100644 --- a/tests/plugins/globalhistory/test_globalhistory.py +++ b/tests/plugins/globalhistory/test_globalhistory.py @@ -5,27 +5,27 @@ from wiki.models import URLPath from ...base import ArticleWebTestUtils, DjangoClientTestBase, RequireRootArticleMixin -class GlobalhistoryTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): - +class GlobalhistoryTests( + RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase +): def test_history(self): - url = reverse('wiki:globalhistory') - url0 = reverse('wiki:globalhistory', kwargs={'only_last': '0'}) - url1 = reverse('wiki:globalhistory', kwargs={'only_last': '1'}) + url = reverse("wiki:globalhistory") + url0 = reverse("wiki:globalhistory", kwargs={"only_last": "0"}) + url1 = reverse("wiki:globalhistory", kwargs={"only_last": "1"}) response = self.client.get(url) - expected = ( - '(?s).*Root Article.*no log message.*' - ) + expected = "(?s).*Root Article.*no log message.*" self.assertRegexpMatches(response.rendered_content, expected) - URLPath.create_urlpath(URLPath.root(), "testhistory1", - title="TestHistory1", content="a page", - user_message="Comment 1") - response = self.client.get(url) - expected = ( - '(?s).*TestHistory1.*Comment 1.*' - 'Root Article.*no log message.*' + URLPath.create_urlpath( + URLPath.root(), + "testhistory1", + title="TestHistory1", + content="a page", + user_message="Comment 1", ) + response = self.client.get(url) + expected = "(?s).*TestHistory1.*Comment 1.*" "Root Article.*no log message.*" self.assertRegexpMatches(response.rendered_content, expected) urlpath = URLPath.create_urlpath( @@ -33,12 +33,12 @@ class GlobalhistoryTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoCli "testhistory2", title="TestHistory2", content="a page", - user_message="Comment 2" + user_message="Comment 2", ) expected = ( - '(?s).*TestHistory2.*Comment 2.*' - 'TestHistory1.*Comment 1.*' - 'Root Article.*no log message.*' + "(?s).*TestHistory2.*Comment 2.*" + "TestHistory1.*Comment 1.*" + "Root Article.*no log message.*" ) response = self.client.get(url) self.assertRegexpMatches(response.rendered_content, expected) @@ -50,20 +50,22 @@ class GlobalhistoryTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoCli self.assertRegexpMatches(response.rendered_content, expected) response = self.client.post( - reverse('wiki:edit', kwargs={'path': 'testhistory2/'}), - {'content': 'a page modified', - 'current_revision': str(urlpath.article.current_revision.id), - 'preview': '0', - 'save': '1', - 'summary': 'Testing Revision', - 'title': 'TestHistory2Mod'} + reverse("wiki:edit", kwargs={"path": "testhistory2/"}), + { + "content": "a page modified", + "current_revision": str(urlpath.article.current_revision.id), + "preview": "0", + "save": "1", + "summary": "Testing Revision", + "title": "TestHistory2Mod", + }, ) expected = ( - '(?s).*TestHistory2Mod.*Testing Revision.*' - 'TestHistory2.*Comment 2.*' - 'TestHistory1.*Comment 1.*' - 'Root Article.*no log message.*' + "(?s).*TestHistory2Mod.*Testing Revision.*" + "TestHistory2.*Comment 2.*" + "TestHistory1.*Comment 1.*" + "Root Article.*no log message.*" ) response = self.client.get(url) self.assertRegexpMatches(response.rendered_content, expected) @@ -72,22 +74,22 @@ class GlobalhistoryTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoCli self.assertRegexpMatches(response.rendered_content, expected) expected = ( - '(?s).*TestHistory2Mod.*Testing Revision.*' - 'TestHistory1.*Comment 1.*' - 'Root Article.*no log message.*' + "(?s).*TestHistory2Mod.*Testing Revision.*" + "TestHistory1.*Comment 1.*" + "Root Article.*no log message.*" ) response = self.client.get(url1) self.assertRegexpMatches(response.rendered_content, expected) def test_translation(self): # Test that translation of "List of %s changes in the wiki." exists. - url = reverse('wiki:globalhistory') + url = reverse("wiki:globalhistory") response_en = self.client.get(url) - self.assertIn('Global history', response_en.rendered_content) - self.assertIn('in the wiki', response_en.rendered_content) + self.assertIn("Global history", response_en.rendered_content) + self.assertIn("in the wiki", response_en.rendered_content) - with translation.override('da-DK'): + with translation.override("da-DK"): response_da = self.client.get(url) - self.assertNotIn('Global history', response_da.rendered_content) - self.assertNotIn('in the wiki', response_da.rendered_content) + self.assertNotIn("Global history", response_da.rendered_content) + self.assertNotIn("in the wiki", response_da.rendered_content) diff --git a/tests/plugins/images/test_forms.py b/tests/plugins/images/test_forms.py index 075ae8f5..d5b4f30d 100644 --- a/tests/plugins/images/test_forms.py +++ b/tests/plugins/images/test_forms.py @@ -5,10 +5,10 @@ from wiki.plugins.images.forms import PurgeForm class PurgeFormTest(TestCase): def test_not_sure(self): - form = PurgeForm(data={'confirm': False}) + form = PurgeForm(data={"confirm": False}) self.assertIs(form.is_valid(), False) - self.assertEqual(form.errors['confirm'], [gettext('You are not sure enough!')]) + self.assertEqual(form.errors["confirm"], [gettext("You are not sure enough!")]) def test_sure(self): - form = PurgeForm(data={'confirm': True}) + form = PurgeForm(data={"confirm": True}) self.assertIs(form.is_valid(), True) diff --git a/tests/plugins/images/test_views.py b/tests/plugins/images/test_views.py index 9899d73b..25441a32 100644 --- a/tests/plugins/images/test_views.py +++ b/tests/plugins/images/test_views.py @@ -10,11 +10,15 @@ from wiki.models import URLPath from wiki.plugins.images import models from wiki.plugins.images.wiki_plugin import ImagePlugin -from ...base import ArticleWebTestUtils, DjangoClientTestBase, RequireRootArticleMixin, wiki_override_settings +from ...base import ( + ArticleWebTestUtils, + DjangoClientTestBase, + RequireRootArticleMixin, + wiki_override_settings, +) class ImageTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): - def setUp(self): super().setUp() self.article = self.root_article @@ -31,16 +35,11 @@ class ImageTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestB Optional Arguments : filename : str, Defaults to 'test.txt' """ - filename = kwargs.get('filename', 'test.gif') + filename = kwargs.get("filename", "test.gif") data = base64.b64decode(str_base64) filedata = BytesIO(data) filestream = InMemoryUploadedFile( - filedata, - None, - filename, - 'image', - len(data), - None + filedata, None, filename, "image", len(data), None ) return filestream @@ -52,24 +51,24 @@ class ImageTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestB plugin_index = cnt break self.assertGreaterEqual(plugin_index, 0, msg="Image plugin not activated") - base_edit_url = reverse('wiki:edit', kwargs={'path': path}) - url = base_edit_url + '?f=form{0:d}'.format(plugin_index) + base_edit_url = reverse("wiki:edit", kwargs={"path": path}) + url = base_edit_url + "?f=form{0:d}".format(plugin_index) filestream = self._create_gif_filestream_from_base64(self.test_data) response = self.client.post( url, { - 'unsaved_article_title': self.article.current_revision.title, - 'unsaved_article_content': self.article.current_revision.content, - 'image': filestream, - 'images_save': '1', + "unsaved_article_title": self.article.current_revision.title, + "unsaved_article_content": self.article.current_revision.content, + "image": filestream, + "images_save": "1", }, ) self.assertRedirects(response, base_edit_url) def test_index(self): - url = reverse('wiki:images_index', kwargs={'path': ''}) + url = reverse("wiki:images_index", kwargs={"path": ""}) response = self.client.get(url,) - self.assertContains(response, 'Images') + self.assertContains(response, "Images") def test_upload(self): """ @@ -77,22 +76,18 @@ class ImageTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestB Uploading a file should preserve the original filename. Uploading should not modify file in any way. """ - self._create_test_image('') + self._create_test_image("") # Check the object was created. image = models.Image.objects.get() image_revision = image.current_revision.imagerevision - self.assertEqual(image_revision.get_filename(), 'test.gif') + self.assertEqual(image_revision.get_filename(), "test.gif") self.assertEqual( - image_revision.image.file.read(), - base64.b64decode(self.test_data) + image_revision.image.file.read(), base64.b64decode(self.test_data) ) def get_article(self, cont, image): urlpath = URLPath.create_urlpath( - URLPath.root(), - "html_image", - title="TestImage", - content=cont + URLPath.root(), "html_image", title="TestImage", content=cont ) if image: self._create_test_image(urlpath.path) @@ -143,68 +138,87 @@ class ImageTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestB # https://gist.github.com/guillaumepiot/817a70706587da3bd862835c59ef584e def generate_photo_file(self): file = BytesIO() - image = Image.new('RGBA', size=(100, 100), color=(155, 0, 0)) - image.save(file, 'gif') - file.name = 'test.gif' + image = Image.new("RGBA", size=(100, 100), color=(155, 0, 0)) + image.save(file, "gif") + file.name = "test.gif" file.seek(0) return file def test_add_revision(self): - self._create_test_image(path='') + self._create_test_image(path="") image = models.Image.objects.get() before_edit_rev = image.current_revision.revision_number response = self.client.post( - reverse('wiki:images_add_revision', kwargs={ - 'article_id': self.root_article, 'image_id': image.pk, 'path': '', - }), - data={'image': self.generate_photo_file()} - ) - self.assertRedirects( - response, reverse('wiki:edit', kwargs={'path': ''}) - ) + reverse( + "wiki:images_add_revision", + kwargs={ + "article_id": self.root_article, + "image_id": image.pk, + "path": "", + }, + ), + data={"image": self.generate_photo_file()}, + ) + self.assertRedirects(response, reverse("wiki:edit", kwargs={"path": ""})) image = models.Image.objects.get() self.assertEqual(models.Image.objects.count(), 1) - self.assertEqual(image.current_revision.previous_revision.revision_number, before_edit_rev) + self.assertEqual( + image.current_revision.previous_revision.revision_number, before_edit_rev + ) def test_delete_restore_revision(self): - self._create_test_image(path='') + self._create_test_image(path="") image = models.Image.objects.get() before_edit_rev = image.current_revision.revision_number response = self.client.get( - reverse('wiki:images_delete', kwargs={ - 'article_id': self.root_article, 'image_id': image.pk, 'path': '', - }), + reverse( + "wiki:images_delete", + kwargs={ + "article_id": self.root_article, + "image_id": image.pk, + "path": "", + }, + ), ) self.assertRedirects( - response, reverse('wiki:images_index', kwargs={'path': ''}) + response, reverse("wiki:images_index", kwargs={"path": ""}) ) image = models.Image.objects.get() self.assertEqual(models.Image.objects.count(), 1) - self.assertEqual(image.current_revision.previous_revision.revision_number, before_edit_rev) + self.assertEqual( + image.current_revision.previous_revision.revision_number, before_edit_rev + ) self.assertIs(image.current_revision.deleted, True) # RESTORE before_edit_rev = image.current_revision.revision_number response = self.client.get( - reverse('wiki:images_restore', kwargs={ - 'article_id': self.root_article, 'image_id': image.pk, 'path': '', - }), + reverse( + "wiki:images_restore", + kwargs={ + "article_id": self.root_article, + "image_id": image.pk, + "path": "", + }, + ), ) self.assertRedirects( - response, reverse('wiki:images_index', kwargs={'path': ''}) + response, reverse("wiki:images_index", kwargs={"path": ""}) ) image = models.Image.objects.get() self.assertEqual(models.Image.objects.count(), 1) - self.assertEqual(image.current_revision.previous_revision.revision_number, before_edit_rev) + self.assertEqual( + image.current_revision.previous_revision.revision_number, before_edit_rev + ) self.assertFalse(image.current_revision.deleted) def test_purge(self): """ Tests that an image is really purged """ - self._create_test_image(path='') + self._create_test_image(path="") image = models.Image.objects.get() image_revision = image.current_revision.imagerevision f_path = image_revision.image.file.name @@ -212,24 +226,30 @@ class ImageTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestB self.assertIs(os.path.exists(f_path), True) response = self.client.post( - reverse('wiki:images_purge', kwargs={ - 'article_id': self.root_article, 'image_id': image.pk, 'path': '', - }), - data={'confirm': True} + reverse( + "wiki:images_purge", + kwargs={ + "article_id": self.root_article, + "image_id": image.pk, + "path": "", + }, + ), + data={"confirm": True}, ) self.assertRedirects( - response, reverse('wiki:images_index', kwargs={'path': ''}) + response, reverse("wiki:images_index", kwargs={"path": ""}) ) self.assertEqual(models.Image.objects.count(), 0) self.assertIs(os.path.exists(f_path), False) @wiki_override_settings(ACCOUNT_HANDLING=True) def test_login_on_revision_add(self): - self._create_test_image(path='') + self._create_test_image(path="") self.client.logout() image = models.Image.objects.get() - url = reverse('wiki:images_add_revision', kwargs={ - 'article_id': self.root_article, 'image_id': image.pk, 'path': '', - }) - response = self.client.post(url, data={'image': self.generate_photo_file()}) - self.assertRedirects(response, '{}?next={}'.format(reverse('wiki:login'), url)) + url = reverse( + "wiki:images_add_revision", + kwargs={"article_id": self.root_article, "image_id": image.pk, "path": "",}, + ) + response = self.client.post(url, data={"image": self.generate_photo_file()}) + self.assertRedirects(response, "{}?next={}".format(reverse("wiki:login"), url)) diff --git a/tests/plugins/links/test_links.py b/tests/plugins/links/test_links.py index 38908d7c..74a47942 100644 --- a/tests/plugins/links/test_links.py +++ b/tests/plugins/links/test_links.py @@ -8,53 +8,52 @@ from wiki.plugins.links.mdx.djangowikilinks import WikiPathExtension class WikiPathExtensionTests(TestCase): def test_works_with_lazy_functions(self): URLPath.create_root() - config = ( - ('base_url', reverse_lazy('wiki:get', kwargs={'path': ''})), - ) - md = markdown.Markdown( - extensions=['extra', WikiPathExtension(config)] - ) - text = '[Français](wiki:/fr)' + config = (("base_url", reverse_lazy("wiki:get", kwargs={"path": ""})),) + md = markdown.Markdown(extensions=["extra", WikiPathExtension(config)]) + text = "[Français](wiki:/fr)" self.assertEqual( md.convert(text), '<p><a class="wikipath linknotfound" href="/fr">Français</a></p>', ) - URLPath.create_urlpath(URLPath.root(), "linktest", - title="LinkTest", - content="A page\n#A section\nA line", - user_message="Comment1") + URLPath.create_urlpath( + URLPath.root(), + "linktest", + title="LinkTest", + content="A page\n#A section\nA line", + user_message="Comment1", + ) # Link to an existing page - text = '[Test link](wiki:/linktest)' + text = "[Test link](wiki:/linktest)" self.assertEqual( md.convert(text), '<p><a class="wikipath" href="/linktest/">Test link</a></p>', ) # Link with an empty fragment - text = '[Test link](wiki:/linktest#)' + text = "[Test link](wiki:/linktest#)" self.assertEqual( md.convert(text), '<p><a class="wikipath" href="/linktest/#">Test link</a></p>', ) # Link to a header in an existing page - text = '[Test head](wiki:/linktest#wiki-toc-a-section)' + text = "[Test head](wiki:/linktest#wiki-toc-a-section)" self.assertEqual( md.convert(text), '<p><a class="wikipath" href="/linktest/#wiki-toc-a-section">Test head</a></p>', ) # Link to a header in a non existing page - text = '[Test head nonExist](wiki:/linktesterr#wiki-toc-a-section)' + text = "[Test head nonExist](wiki:/linktesterr#wiki-toc-a-section)" self.assertEqual( md.convert(text), '<p><a class="wikipath linknotfound" href="/linktesterr#wiki-toc-a-section">Test head nonExist</a></p>', ) # Invalid Wiki link: The default markdown link parser takes over - text = '[Test head err](wiki:/linktest#wiki-toc-a-section#err)' + text = "[Test head err](wiki:/linktest#wiki-toc-a-section#err)" self.assertEqual( md.convert(text), '<p><a href="wiki:/linktest#wiki-toc-a-section#err">Test head err</a></p>', diff --git a/tests/plugins/links/test_urlize.py b/tests/plugins/links/test_urlize.py index cf23eead..0c3f2f16 100644 --- a/tests/plugins/links/test_urlize.py +++ b/tests/plugins/links/test_urlize.py @@ -8,226 +8,178 @@ from wiki.plugins.links.mdx.urlize import UrlizeExtension, makeExtension EXPECTED_LINK_TEMPLATE = ( '<a href="%s" rel="nofollow" target="_blank">' '<span class="fa fa-external-link">' - '</span>' - '<span>' - ' %s' - '</span>' - '</a>' + "</span>" + "<span>" + " %s" + "</span>" + "</a>" ) # Template accepts two strings - href value and link text value. -EXPECTED_PARAGRAPH_TEMPLATE = '<p>%s</p>' % EXPECTED_LINK_TEMPLATE +EXPECTED_PARAGRAPH_TEMPLATE = "<p>%s</p>" % EXPECTED_LINK_TEMPLATE FIXTURE_POSITIVE_MATCHES = [ # Test surrounding begin/end characters. ( - '(example.com)', - '<p>(' + EXPECTED_LINK_TEMPLATE % ('http://example.com', 'example.com') + ')</p>' + "(example.com)", + "<p>(" + + EXPECTED_LINK_TEMPLATE % ("http://example.com", "example.com") + + ")</p>", ), ( - '<example.com>', - '<p><' + EXPECTED_LINK_TEMPLATE % ('http://example.com', 'example.com') + '></p>' + "<example.com>", + "<p><" + + EXPECTED_LINK_TEMPLATE % ("http://example.com", "example.com") + + "></p>", ), - # Test protocol specification. ( - 'http://example.com', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://example.com', 'http://example.com') + "http://example.com", + EXPECTED_PARAGRAPH_TEMPLATE % ("http://example.com", "http://example.com"), ), ( - 'https://example.com', - EXPECTED_PARAGRAPH_TEMPLATE % ('https://example.com', 'https://example.com') + "https://example.com", + EXPECTED_PARAGRAPH_TEMPLATE % ("https://example.com", "https://example.com"), ), ( - 'ftp://example.com', - EXPECTED_PARAGRAPH_TEMPLATE % ('ftp://example.com', 'ftp://example.com') + "ftp://example.com", + EXPECTED_PARAGRAPH_TEMPLATE % ("ftp://example.com", "ftp://example.com"), ), ( - 'ftps://example.com', - EXPECTED_PARAGRAPH_TEMPLATE % ('ftps://example.com', 'ftps://example.com') + "ftps://example.com", + EXPECTED_PARAGRAPH_TEMPLATE % ("ftps://example.com", "ftps://example.com"), ), ( - 'example.com', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://example.com', 'example.com') + "example.com", + EXPECTED_PARAGRAPH_TEMPLATE % ("http://example.com", "example.com"), ), ( - 'onion://example.com', - EXPECTED_PARAGRAPH_TEMPLATE % ('onion://example.com', 'onion://example.com') + "onion://example.com", + EXPECTED_PARAGRAPH_TEMPLATE % ("onion://example.com", "onion://example.com"), ), ( - 'onion9+.-://example.com', - EXPECTED_PARAGRAPH_TEMPLATE % ('onion9+.-://example.com', 'onion9+.-://example.com') + "onion9+.-://example.com", + EXPECTED_PARAGRAPH_TEMPLATE + % ("onion9+.-://example.com", "onion9+.-://example.com"), ), - # Test various supported host variations. + ("10.10.1.1", EXPECTED_PARAGRAPH_TEMPLATE % ("http://10.10.1.1", "10.10.1.1")), ( - '10.10.1.1', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://10.10.1.1', '10.10.1.1') - ), - ( - '1122:3344:5566:7788:9900:aabb:ccdd:eeff', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://1122:3344:5566:7788:9900:aabb:ccdd:eeff', '1122:3344:5566:7788:9900:aabb:ccdd:eeff') - ), - ( - '1122:3344:5566:7788:9900:AaBb:cCdD:EeFf', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://1122:3344:5566:7788:9900:AaBb:cCdD:EeFf', '1122:3344:5566:7788:9900:AaBb:cCdD:EeFf') - ), - ( - '::1', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://::1', '::1') - ), - ( - '1::2:3', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://1::2:3', '1::2:3') + "1122:3344:5566:7788:9900:aabb:ccdd:eeff", + EXPECTED_PARAGRAPH_TEMPLATE + % ( + "http://1122:3344:5566:7788:9900:aabb:ccdd:eeff", + "1122:3344:5566:7788:9900:aabb:ccdd:eeff", + ), ), ( - '1::', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://1::', '1::') + "1122:3344:5566:7788:9900:AaBb:cCdD:EeFf", + EXPECTED_PARAGRAPH_TEMPLATE + % ( + "http://1122:3344:5566:7788:9900:AaBb:cCdD:EeFf", + "1122:3344:5566:7788:9900:AaBb:cCdD:EeFf", + ), ), + ("::1", EXPECTED_PARAGRAPH_TEMPLATE % ("http://::1", "::1")), + ("1::2:3", EXPECTED_PARAGRAPH_TEMPLATE % ("http://1::2:3", "1::2:3")), + ("1::", EXPECTED_PARAGRAPH_TEMPLATE % ("http://1::", "1::")), + ("::", EXPECTED_PARAGRAPH_TEMPLATE % ("http://::", "::")), ( - '::', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://::', '::') + "example.com", + EXPECTED_PARAGRAPH_TEMPLATE % ("http://example.com", "example.com"), ), ( - 'example.com', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://example.com', 'example.com') + "example.horse", + EXPECTED_PARAGRAPH_TEMPLATE % ("http://example.horse", "example.horse"), ), ( - 'example.horse', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://example.horse', 'example.horse') + "my.long.domain.example.com", + EXPECTED_PARAGRAPH_TEMPLATE + % ("http://my.long.domain.example.com", "my.long.domain.example.com"), ), - ( - 'my.long.domain.example.com', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://my.long.domain.example.com', 'my.long.domain.example.com') - ), - # Test port section. - ( - '10.1.1.1:8000', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://10.1.1.1:8000', '10.1.1.1:8000') + "10.1.1.1:8000", + EXPECTED_PARAGRAPH_TEMPLATE % ("http://10.1.1.1:8000", "10.1.1.1:8000"), ), - # Test trailing path specification. ( - 'http://example.com/', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://example.com/', 'http://example.com/') + "http://example.com/", + EXPECTED_PARAGRAPH_TEMPLATE % ("http://example.com/", "http://example.com/"), ), ( - 'http://example.com/my/path', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://example.com/my/path', 'http://example.com/my/path') + "http://example.com/my/path", + EXPECTED_PARAGRAPH_TEMPLATE + % ("http://example.com/my/path", "http://example.com/my/path"), ), ( - 'http://example.com/my/path?param1=value1¶m2=value2', - EXPECTED_PARAGRAPH_TEMPLATE % ('http://example.com/my/path?param1=value1&param2=value2', 'http://example.com/my/path?param1=value1&param2=value2') + "http://example.com/my/path?param1=value1¶m2=value2", + EXPECTED_PARAGRAPH_TEMPLATE + % ( + "http://example.com/my/path?param1=value1&param2=value2", + "http://example.com/my/path?param1=value1&param2=value2", + ), ), - # Link positioned somewhere within the text, but around whitespace boundary. ( - 'This is link myhost.example.com', - "<p>This is link " + EXPECTED_LINK_TEMPLATE % ('http://myhost.example.com', 'myhost.example.com') + "</p>" + "This is link myhost.example.com", + "<p>This is link " + + EXPECTED_LINK_TEMPLATE % ("http://myhost.example.com", "myhost.example.com") + + "</p>", ), ( - 'myhost.example.com is the link', - "<p>" + EXPECTED_LINK_TEMPLATE % ('http://myhost.example.com', 'myhost.example.com') + " is the link</p>" + "myhost.example.com is the link", + "<p>" + + EXPECTED_LINK_TEMPLATE % ("http://myhost.example.com", "myhost.example.com") + + " is the link</p>", ), ( - 'I have best myhost.example.com link ever', - "<p>I have best " + EXPECTED_LINK_TEMPLATE % ('http://myhost.example.com', 'myhost.example.com') + " link ever</p>" + "I have best myhost.example.com link ever", + "<p>I have best " + + EXPECTED_LINK_TEMPLATE % ("http://myhost.example.com", "myhost.example.com") + + " link ever</p>", ), ( - 'I have best\nmyhost.example.com link ever', - "<p>I have best\n" + EXPECTED_LINK_TEMPLATE % ('http://myhost.example.com', 'myhost.example.com') + " link ever</p>" + "I have best\nmyhost.example.com link ever", + "<p>I have best\n" + + EXPECTED_LINK_TEMPLATE % ("http://myhost.example.com", "myhost.example.com") + + " link ever</p>", ), ] FIXTURE_NEGATIVE_MATCHES = [ # localhost as part of another word. - ( - 'localhosts', - '<p>localhosts</p>' - ), - ( - 'localhost', - '<p>localhost</p>' - - ), - ( - 'localhost:8000', - '<p>localhost:8000</p>' - ), - + ("localhosts", "<p>localhosts</p>"), + ("localhost", "<p>localhost</p>"), + ("localhost:8000", "<p>localhost:8000</p>"), # Incomplete FQDNs. - ( - 'example.', - '<p>example.</p>' - ), - ( - '.example .com', - '<p>.example .com</p>' - ), + ("example.", "<p>example.</p>"), + (".example .com", "<p>.example .com</p>"), # Invalid FQDNs. - ( - 'example-.com', - '<p>example-.com</p>' - ), - ( - '-example.com', - '<p>-example.com</p>' - ), - ( - 'my.-example.com', - '<p>my.-example.com</p>' - ), - + ("example-.com", "<p>example-.com</p>"), + ("-example.com", "<p>-example.com</p>"), + ("my.-example.com", "<p>my.-example.com</p>"), # Invalid IPv6 patterns. ( - '1:2:3:4:5:6:7:8:a', # Use :a, because using a number would match as optional port - '<p>1:2:3:4:5:6:7:8:a</p>', - ), - ( - '1::2::3', - '<p>1::2::3</p>', - ), - ( - '::::1', - '<p>::::1</p>', - ), - ( - '1::::', - '<p>1::::</p>', + "1:2:3:4:5:6:7:8:a", # Use :a, because using a number would match as optional port + "<p>1:2:3:4:5:6:7:8:a</p>", ), - + ("1::2::3", "<p>1::2::3</p>",), + ("::::1", "<p>::::1</p>",), + ("1::::", "<p>1::::</p>",), # Invalid IPv4 patterns. - ( - '1.2.3.4.5', - '<p>1.2.3.4.5</p>', - ), - + ("1.2.3.4.5", "<p>1.2.3.4.5</p>",), # Invalid protocols. - ( - '9onion://example.com', - '<p>9onion://example.com</p>', - ), - ( - '-onion://example.com', - '<p>-onion://example.com</p>', - ), - ( - '+onion://example.com', - '<p>+onion://example.com</p>', - ), - ( - '.onion://example.com', - '<p>.onion://example.com</p>', - ), + ("9onion://example.com", "<p>9onion://example.com</p>",), + ("-onion://example.com", "<p>-onion://example.com</p>",), + ("+onion://example.com", "<p>+onion://example.com</p>",), + (".onion://example.com", "<p>.onion://example.com</p>",), ] class TestUrlizeExtension: - def setup_method(self): self.md = markdown.Markdown(extensions=[UrlizeExtension()]) @@ -240,12 +192,24 @@ class TestUrlizeExtension: assert self.md.convert(markdown_text) == expected_output def test_url_with_non_matching_begin_and_end_ignored(self): - assert self.md.convert('(example.com>') == "<p>%s</p>" % html.escape('(example.com>') - assert self.md.convert('<example.com)') == "<p>%s</p>" % html.escape('<example.com)') - assert self.md.convert('(example.com') == "<p>%s</p>" % html.escape('(example.com') - assert self.md.convert('example.com)') == "<p>%s</p>" % html.escape('example.com)') - assert self.md.convert('<example.com') == "<p>%s</p>" % html.escape('<example.com') - assert self.md.convert('example.com>') == "<p>%s</p>" % html.escape('example.com>') + assert self.md.convert("(example.com>") == "<p>%s</p>" % html.escape( + "(example.com>" + ) + assert self.md.convert("<example.com)") == "<p>%s</p>" % html.escape( + "<example.com)" + ) + assert self.md.convert("(example.com") == "<p>%s</p>" % html.escape( + "(example.com" + ) + assert self.md.convert("example.com)") == "<p>%s</p>" % html.escape( + "example.com)" + ) + assert self.md.convert("<example.com") == "<p>%s</p>" % html.escape( + "<example.com" + ) + assert self.md.convert("example.com>") == "<p>%s</p>" % html.escape( + "example.com>" + ) def test_makeExtension_return_value(): diff --git a/tests/plugins/macros/test_links.py b/tests/plugins/macros/test_links.py index 05267b4f..b1650f9c 100644 --- a/tests/plugins/macros/test_links.py +++ b/tests/plugins/macros/test_links.py @@ -5,7 +5,8 @@ from wiki.core import markdown class WikiLinksTests(RequireRootArticleMixin, TestBase): def test_wikilink(self): md = markdown.ArticleMarkdown(article=self.root_article) - md_text = md.convert('[[Root Article]]') + md_text = md.convert("[[Root Article]]") self.assertEqual( - md_text, '<p><a class="wiki_wikilink wiki-external" href="/Root_Article/">Root Article</a></p>' + md_text, + '<p><a class="wiki_wikilink wiki-external" href="/Root_Article/">Root Article</a></p>', ) diff --git a/tests/plugins/macros/test_toc.py b/tests/plugins/macros/test_toc.py index d5b24a0f..054df8b0 100644 --- a/tests/plugins/macros/test_toc.py +++ b/tests/plugins/macros/test_toc.py @@ -7,9 +7,7 @@ class TocMacroTests(TestCase): def test_toc_renders_table_of_content(self): """ Verifies that the [TOC] wiki code renders a Table of Content """ - md = markdown.Markdown( - extensions=['extra', WikiTocExtension()] - ) + md = markdown.Markdown(extensions=["extra", WikiTocExtension()]) text = ( "[TOC]\n" "\n" @@ -23,16 +21,16 @@ class TocMacroTests(TestCase): ) expected_output = ( '<div class="toc">\n' - '<ul>\n' + "<ul>\n" '<li><a href="#wiki-toc-first-title">First title.</a><ul>\n' '<li><a href="#wiki-toc-subsection">Subsection</a></li>\n' - '</ul>\n' - '</li>\n' - '</ul>\n' - '</div>\n' + "</ul>\n" + "</li>\n" + "</ul>\n" + "</div>\n" '<h1 id="wiki-toc-first-title">First title.</h1>\n' - '<p>Paragraph 1</p>\n' + "<p>Paragraph 1</p>\n" '<h2 id="wiki-toc-subsection">Subsection</h2>\n' - '<p>Paragraph 2</p>' + "<p>Paragraph 2</p>" ) self.assertEqual(md.convert(text), expected_output) diff --git a/tests/plugins/notifications/test_forms.py b/tests/plugins/notifications/test_forms.py index cc62d001..28eadacd 100644 --- a/tests/plugins/notifications/test_forms.py +++ b/tests/plugins/notifications/test_forms.py @@ -8,4 +8,3 @@ from wiki.plugins.notifications.forms import SettingsFormSet class SettingsFormTests(RequireSuperuserMixin, TestCase): def test_formset(self): formset = SettingsFormSet(user=self.superuser1) - diff --git a/tests/plugins/notifications/test_views.py b/tests/plugins/notifications/test_views.py index 26787869..0b899ce5 100644 --- a/tests/plugins/notifications/test_views.py +++ b/tests/plugins/notifications/test_views.py @@ -1,58 +1,69 @@ from django.shortcuts import resolve_url from django_nyt.models import Settings -from tests.base import ArticleWebTestUtils, DjangoClientTestBase, RequireRootArticleMixin +from tests.base import ( + ArticleWebTestUtils, + DjangoClientTestBase, + RequireRootArticleMixin, +) -class NotificationSettingsTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase): - +class NotificationSettingsTests( + RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase +): def setUp(self): super().setUp() def test_login_required(self): self.client.logout() - response = self.client.get(resolve_url('wiki:notification_settings')) + response = self.client.get(resolve_url("wiki:notification_settings")) self.assertEqual(response.status_code, 302) def test_when_logged_in(self): - response = self.client.get(resolve_url('wiki:notification_settings')) + response = self.client.get(resolve_url("wiki:notification_settings")) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wiki/plugins/notifications/settings.html') + self.assertTemplateUsed(response, "wiki/plugins/notifications/settings.html") def test_change_settings(self): - self.settings, __ = Settings.objects.get_or_create(user=self.superuser1, is_default=True) + self.settings, __ = Settings.objects.get_or_create( + user=self.superuser1, is_default=True + ) - url = resolve_url('wiki:notification_settings') + url = resolve_url("wiki:notification_settings") response = self.client.get(url) self.assertEqual(response.status_code, 200) - data = {'csrf_token': response.context['csrf_token']} + data = {"csrf_token": response.context["csrf_token"]} # management form information, needed because of the formset - management_form = response.context['form'].management_form + management_form = response.context["form"].management_form - for i in 'TOTAL_FORMS', 'INITIAL_FORMS', 'MIN_NUM_FORMS', 'MAX_NUM_FORMS': - data['%s-%s' % (management_form.prefix, i)] = management_form[i].value() + for i in "TOTAL_FORMS", "INITIAL_FORMS", "MIN_NUM_FORMS", "MAX_NUM_FORMS": + data["%s-%s" % (management_form.prefix, i)] = management_form[i].value() - for i in range(response.context['form'].total_form_count()): + for i in range(response.context["form"].total_form_count()): # get form index 'i' - current_form = response.context['form'].forms[i] + current_form = response.context["form"].forms[i] # retrieve all the fields for field_name in current_form.fields: value = current_form[field_name].value() - data['%s-%s' % (current_form.prefix, field_name)] = value if value is not None else '' + data["%s-%s" % (current_form.prefix, field_name)] = ( + value if value is not None else "" + ) - data['form-TOTAL_FORMS'] = 1 - data['form-0-email'] = 2 - data['form-0-interval'] = 0 + data["form-TOTAL_FORMS"] = 1 + data["form-0-email"] = 2 + data["form-0-interval"] = 0 # post the request without any change response = self.client.post(url, data, follow=True) - self.assertEqual(len(response.context.get('messages')), 1) + self.assertEqual(len(response.context.get("messages")), 1) - message = response.context.get('messages')._loaded_messages[0] - self.assertIn(message.message, 'You will receive notifications instantly for 0 articles') + message = response.context.get("messages")._loaded_messages[0] + self.assertIn( + message.message, "You will receive notifications instantly for 0 articles" + ) # Ensure we didn't create redundant Settings objects assert self.superuser1.nyt_settings.all().count() == 1 diff --git a/tests/settings.py b/tests/settings.py index 7ed1c1e8..2a3e30fc 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -4,57 +4,53 @@ from django.urls import reverse_lazy TESTS_DATA_ROOT = os.path.dirname(__file__) -MEDIA_ROOT = os.path.join(TESTS_DATA_ROOT, 'media') +MEDIA_ROOT = os.path.join(TESTS_DATA_ROOT, "media") -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - } -} +DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3",}} DEBUG = True -AUTH_USER_MODEL = 'testdata.CustomUser' -WIKI_GROUP_MODEL = 'testdata.CustomGroup' +AUTH_USER_MODEL = "testdata.CustomUser" +WIKI_GROUP_MODEL = "testdata.CustomGroup" SITE_ID = 1 -ROOT_URLCONF = 'tests.testdata.urls' +ROOT_URLCONF = "tests.testdata.urls" INSTALLED_APPS = [ - 'tests.testdata', - 'django.contrib.auth.apps.AuthConfig', - 'django.contrib.contenttypes.apps.ContentTypesConfig', - 'django.contrib.sessions.apps.SessionsConfig', - 'django.contrib.admin.apps.AdminConfig', - 'django.contrib.humanize.apps.HumanizeConfig', - 'django.contrib.sites.apps.SitesConfig', - 'django.contrib.messages', - 'django_nyt.apps.DjangoNytConfig', - 'mptt', - 'sekizai', - 'sorl.thumbnail', - 'wiki.apps.WikiConfig', - 'wiki.plugins.attachments.apps.AttachmentsConfig', - 'wiki.plugins.editsection.apps.EditSectionConfig', - 'wiki.plugins.notifications.apps.NotificationsConfig', - 'wiki.plugins.images.apps.ImagesConfig', - 'wiki.plugins.macros.apps.MacrosConfig', - 'wiki.plugins.globalhistory.apps.GlobalHistoryConfig', + "tests.testdata", + "django.contrib.auth.apps.AuthConfig", + "django.contrib.contenttypes.apps.ContentTypesConfig", + "django.contrib.sessions.apps.SessionsConfig", + "django.contrib.admin.apps.AdminConfig", + "django.contrib.humanize.apps.HumanizeConfig", + "django.contrib.sites.apps.SitesConfig", + "django.contrib.messages", + "django_nyt.apps.DjangoNytConfig", + "mptt", + "sekizai", + "sorl.thumbnail", + "wiki.apps.WikiConfig", + "wiki.plugins.attachments.apps.AttachmentsConfig", + "wiki.plugins.editsection.apps.EditSectionConfig", + "wiki.plugins.notifications.apps.NotificationsConfig", + "wiki.plugins.images.apps.ImagesConfig", + "wiki.plugins.macros.apps.MacrosConfig", + "wiki.plugins.globalhistory.apps.GlobalHistoryConfig", "wiki.plugins.redlinks.apps.RedlinksConfig", ] MIDDLEWARE = [ - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", ] USE_TZ = True -SECRET_KEY = 'b^fv_)t39h%9p40)fnkfblo##jkr!$0)lkp6bpy!fi*f$4*92!' -STATIC_URL = '/static/' +SECRET_KEY = "b^fv_)t39h%9p40)fnkfblo##jkr!$0)lkp6bpy!fi*f$4*92!" +STATIC_URL = "/static/" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ "django.contrib.auth.context_processors.auth", "django.template.context_processors.debug", "django.template.context_processors.i18n", @@ -69,4 +65,4 @@ TEMPLATES = [ }, ] -LOGIN_REDIRECT_URL = reverse_lazy('wiki:get', kwargs={'path': ''}) +LOGIN_REDIRECT_URL = reverse_lazy("wiki:get", kwargs={"path": ""}) diff --git a/tests/testdata/migrations/0001_initial.py b/tests/testdata/migrations/0001_initial.py index 1e586cb1..86a189a5 100644 --- a/tests/testdata/migrations/0001_initial.py +++ b/tests/testdata/migrations/0001_initial.py @@ -11,53 +11,158 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0008_alter_user_username_max_length'), + ("auth", "0008_alter_user_username_max_length"), ] operations = [ migrations.CreateModel( - name='CustomGroup', + name="CustomGroup", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), ], ), migrations.CreateModel( - name='VeryCustomUser', + name="VeryCustomUser", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('identifier', models.IntegerField()), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ("identifier", models.IntegerField()), ], - options={ - 'abstract': False, - }, + options={"abstract": False,}, ), migrations.CreateModel( - name='CustomUser', + name="CustomUser", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('some_field', models.IntegerField(default=0)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=30, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ("some_field", models.IntegerField(default=0)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), ], options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], + managers=[("objects", django.contrib.auth.models.UserManager()),], ), ] diff --git a/tests/testdata/models.py b/tests/testdata/models.py index 5d7a3307..8f020075 100644 --- a/tests/testdata/models.py +++ b/tests/testdata/models.py @@ -14,4 +14,4 @@ class CustomGroup(models.Model): # user with invalid renamed identifier, and no email field class VeryCustomUser(AbstractBaseUser): identifier = models.IntegerField() - USERNAME_FIELD = 'identifier' + USERNAME_FIELD = "identifier" diff --git a/tests/testdata/urls.py b/tests/testdata/urls.py index cdb4c5bd..683fe18d 100644 --- a/tests/testdata/urls.py +++ b/tests/testdata/urls.py @@ -4,21 +4,22 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.urls import include, re_path urlpatterns = [ - re_path(r'^admin/doc/', include('django.contrib.admindocs.urls')), - re_path(r'^admin/', admin.site.urls), + re_path(r"^admin/doc/", include("django.contrib.admindocs.urls")), + re_path(r"^admin/", admin.site.urls), ] if settings.DEBUG: urlpatterns += staticfiles_urlpatterns() urlpatterns += [ - re_path(r'^media/(?P<path>.*)$', - 'django.views.static.serve', - {'document_root': settings.MEDIA_ROOT, - }), + re_path( + r"^media/(?P<path>.*)$", + "django.views.static.serve", + {"document_root": settings.MEDIA_ROOT,}, + ), ] urlpatterns += [ - re_path(r'^django_functest/', include('django_functest.urls')), - re_path(r'^notify/', include('django_nyt.urls')), - re_path(r'', include('wiki.urls')), + re_path(r"^django_functest/", include("django_functest.urls")), + re_path(r"^notify/", include("django_nyt.urls")), + re_path(r"", include("wiki.urls")), ] -- 2.45.2