M .pre-commit-config.yaml => .pre-commit-config.yaml +5 -6
@@ 1,5 1,5 @@
repos:
-- repo: git://github.com/pre-commit/pre-commit-hooks
+ - repo: git://github.com/pre-commit/pre-commit-hooks
sha: v1.2.0
hooks:
- id: trailing-whitespace
@@ 9,9 9,8 @@ repos:
- id: check-added-large-files
- id: debug-statements
- id: end-of-file-fixer
-- repo: git://github.com/FalconSocial/pre-commit-python-sorter
- sha: b57843b0b874df1d16eb0bef00b868792cb245c2
+ - repo: https://github.com/psf/black
+ rev: stable
hooks:
- - id: python-import-sorter
- args:
- - --silent-overwrite
+ - id: black
+ language_version: python3.6
M docs/conf.py => docs/conf.py +42 -35
@@ 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",
),
]
M setup.py => setup.py +39 -39
@@ 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,
M src/wiki/__init__.py => src/wiki/__init__.py +2 -2
@@ 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)
M src/wiki/admin.py => src/wiki/admin.py +26 -19
@@ 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):
"""
M src/wiki/apps.py => src/wiki/apps.py +13 -4
@@ 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()
M src/wiki/checks.py => src/wiki/checks.py +35 -23
@@ 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
M src/wiki/conf/settings.py => src/wiki/conf/settings.py +91 -133
@@ 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)
M src/wiki/core/exceptions.py => src/wiki/core/exceptions.py +1 -1
@@ 1,10 1,10 @@
-
# If no root URL is found, we raise this...
class NoRootURL(Exception):
pass
+
# If there is more than one...
M src/wiki/core/http.py => src/wiki/core/http.py +8 -5
@@ 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
M src/wiki/core/markdown/__init__.py => src/wiki/core/markdown/__init__.py +9 -11
@@ 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
M src/wiki/core/markdown/mdx/codehilite.py => src/wiki/core/markdown/mdx/codehilite.py +24 -22
@@ 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<fence>^(?:~{3,}|`{3,}))[ ]* # Opening ``` or ~~~
(\{?\.?(?P<lang>[a-zA-Z0-9_+-]*))?[ ]* # Optional {, and lang
# Optional highlight lines, single- or double-quote-delimited
(hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))?[ ]*
}?[ ]*\n # Optional closing }
(?P<code>.*?)(?<=\n)
-(?P=fence)[ ]*$''', re.MULTILINE | re.DOTALL | re.VERBOSE)
- CODE_WRAP = '<pre>%s</pre>'
+(?P=fence)[ ]*$""",
+ re.MULTILINE | re.DOTALL | re.VERBOSE,
+ )
+ CODE_WRAP = "<pre>%s</pre>"
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)
M src/wiki/core/markdown/mdx/previewlinks.py => src/wiki/core/markdown/mdx/previewlinks.py +4 -5
@@ 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
M src/wiki/core/markdown/mdx/responsivetable.py => src/wiki/core/markdown/mdx/responsivetable.py +7 -7
@@ 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):
M src/wiki/core/paginator.py => src/wiki/core/paginator.py +4 -3
@@ 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]
M src/wiki/core/permissions.py => src/wiki/core/permissions.py +10 -13
@@ 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")
M src/wiki/core/plugins/base.py => src/wiki/core/plugins/base.py +8 -7
@@ 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
M src/wiki/core/plugins/loader.py => src/wiki/core/plugins/loader.py +1 -1
@@ 2,4 2,4 @@ from django.utils.module_loading import autodiscover_modules
def load_wiki_plugins():
- autodiscover_modules('wiki_plugin')
+ autodiscover_modules("wiki_plugin")
M src/wiki/core/plugins/registry.py => src/wiki/core/plugins/registry.py +8 -20
@@ 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():
M src/wiki/core/utils.py => src/wiki/core/utils.py +1 -1
@@ 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},
)
M src/wiki/core/version.py => src/wiki/core/version.py +17 -14
@@ 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")
M src/wiki/decorators.py => src/wiki/decorators.py +48 -35
@@ 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
M src/wiki/editors/base.py => src/wiki/editors/base.py +1 -1
@@ 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 = ()
M src/wiki/editors/markitup.py => src/wiki/editors/markitup.py +22 -16
@@ 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",
+ )
M src/wiki/forms.py => src/wiki/forms.py +238 -202
@@ 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 _.<br>Note: If you change the slug later on, links pointing to this article are <b>not</b> 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 _.<br>Note: If you change the slug later on, links pointing to this article are <b>not</b> 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,
+ )
M src/wiki/forms_account_handling.py => src/wiki/forms_account_handling.py +26 -15
@@ 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"))
M src/wiki/managers.py => src/wiki/managers.py +27 -32
@@ 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()
M src/wiki/migrations/0001_initial.py => src/wiki/migrations/0001_initial.py +361 -114
@@ 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,
),
]
M src/wiki/migrations/0002_urlpath_moved_to.py => src/wiki/migrations/0002_urlpath_moved_to.py +12 -4
@@ 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",
+ ),
),
]
M src/wiki/models/__init__.py => src/wiki/models/__init__.py +7 -7
@@ 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)
M src/wiki/models/article.py => src/wiki/models/article.py +105 -92
@@ 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!
M src/wiki/models/pluginbase.py => src/wiki/models/pluginbase.py +53 -47
@@ 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()
M src/wiki/models/urlpath.py => src/wiki/models/urlpath.py +86 -100
@@ 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())
M src/wiki/plugins/attachments/__init__.py => src/wiki/plugins/attachments/__init__.py +1 -1
@@ 1,1 1,1 @@
-default_app_config = 'wiki.plugins.attachments.apps.AttachmentsConfig'
+default_app_config = "wiki.plugins.attachments.apps.AttachmentsConfig"
M src/wiki/plugins/attachments/admin.py => src/wiki/plugins/attachments/admin.py +1 -1
@@ 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):
M src/wiki/plugins/attachments/apps.py => src/wiki/plugins/attachments/apps.py +2 -2
@@ 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"
M src/wiki/plugins/attachments/forms.py => src/wiki/plugins/attachments/forms.py +46 -36
@@ 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"}),
)
M src/wiki/plugins/attachments/markdown_extensions.py => src/wiki/plugins/attachments/markdown_extensions.py +25 -26
@@ 8,8 8,9 @@ from wiki.core.permissions import can_read
from wiki.plugins.attachments import models
ATTACHMENT_RE = re.compile(
- r'(?P<before>.*)\[( *((attachment\:(?P<id>[0-9]+))|(title\:\"(?P<title>[^\"]+)\")|(?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
M src/wiki/plugins/attachments/migrations/0001_initial.py => src/wiki/plugins/attachments/migrations/0001_initial.py +100 -29
@@ 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,
),
]
M src/wiki/plugins/attachments/migrations/0002_auto_20151118_1816.py => src/wiki/plugins/attachments/migrations/0002_auto_20151118_1816.py +3 -5
@@ 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",
),
]
M src/wiki/plugins/attachments/models.py => src/wiki/plugins/attachments/models.py +56 -47
@@ 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!
M src/wiki/plugins/attachments/settings.py => src/wiki/plugins/attachments/settings.py +14 -20
@@ 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."
+ )
M src/wiki/plugins/attachments/urls.py => src/wiki/plugins/attachments/urls.py +32 -20
@@ 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",
+ ),
]
M src/wiki/plugins/attachments/views.py => src/wiki/plugins/attachments/views.py +143 -125
@@ 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
M src/wiki/plugins/attachments/wiki_plugin.py => src/wiki/plugins/attachments/wiki_plugin.py +15 -15
@@ 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()]
M src/wiki/plugins/editsection/__init__.py => src/wiki/plugins/editsection/__init__.py +1 -1
@@ 1,1 1,1 @@
-default_app_config = 'wiki.plugins.editsection.apps.EditSectionConfig'
+default_app_config = "wiki.plugins.editsection.apps.EditSectionConfig"
M src/wiki/plugins/editsection/apps.py => src/wiki/plugins/editsection/apps.py +2 -2
@@ 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"
M src/wiki/plugins/editsection/markdown_extensions.py => src/wiki/plugins/editsection/markdown_extensions.py +27 -22
@@ 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
M src/wiki/plugins/editsection/settings.py => src/wiki/plugins/editsection/settings.py +3 -3
@@ 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]"))
M src/wiki/plugins/editsection/views.py => src/wiki/plugins/editsection/views.py +48 -41
@@ 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()
M src/wiki/plugins/editsection/wiki_plugin.py => src/wiki/plugins/editsection/wiki_plugin.py +9 -5
@@ 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()]
M src/wiki/plugins/globalhistory/__init__.py => src/wiki/plugins/globalhistory/__init__.py +1 -1
@@ 1,1 1,1 @@
-default_app_config = 'wiki.plugins.globalhistory.apps.GlobalHistoryConfig'
+default_app_config = "wiki.plugins.globalhistory.apps.GlobalHistoryConfig"
M src/wiki/plugins/globalhistory/apps.py => src/wiki/plugins/globalhistory/apps.py +2 -2
@@ 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"
M src/wiki/plugins/globalhistory/settings.py => src/wiki/plugins/globalhistory/settings.py +1 -1
@@ 1,1 1,1 @@
-SLUG = 'globalhistory'
+SLUG = "globalhistory"
M src/wiki/plugins/globalhistory/views.py => src/wiki/plugins/globalhistory/views.py +12 -10
@@ 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)
M src/wiki/plugins/globalhistory/wiki_plugin.py => src/wiki/plugins/globalhistory/wiki_plugin.py +10 -4
@@ 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)
M src/wiki/plugins/help/__init__.py => src/wiki/plugins/help/__init__.py +1 -1
@@ 1,1 1,1 @@
-default_app_config = 'wiki.plugins.help.apps.HelpConfig'
+default_app_config = "wiki.plugins.help.apps.HelpConfig"
M src/wiki/plugins/help/apps.py => src/wiki/plugins/help/apps.py +2 -2
@@ 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"
M src/wiki/plugins/help/wiki_plugin.py => src/wiki/plugins/help/wiki_plugin.py +8 -6
@@ 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 = []
M src/wiki/plugins/images/__init__.py => src/wiki/plugins/images/__init__.py +1 -1
@@ 1,1 1,1 @@
-default_app_config = 'wiki.plugins.images.apps.ImagesConfig'
+default_app_config = "wiki.plugins.images.apps.ImagesConfig"
M src/wiki/plugins/images/admin.py => src/wiki/plugins/images/admin.py +6 -7
@@ 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):
M src/wiki/plugins/images/apps.py => src/wiki/plugins/images/apps.py +6 -3
@@ 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,
+ )
M src/wiki/plugins/images/checks.py => src/wiki/plugins/images/checks.py +3 -1
@@ 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
M src/wiki/plugins/images/forms.py => src/wiki/plugins/images/forms.py +17 -15
@@ 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
M src/wiki/plugins/images/markdown_extensions.py => src/wiki/plugins/images/markdown_extensions.py +13 -9
@@ 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
M src/wiki/plugins/images/migrations/0001_initial.py => src/wiki/plugins/images/migrations/0001_initial.py +44 -17
@@ 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",),
),
]
M src/wiki/plugins/images/migrations/0002_auto_20151118_1811.py => src/wiki/plugins/images/migrations/0002_auto_20151118_1811.py +3 -7
@@ 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",
),
]
M src/wiki/plugins/images/models.py => src/wiki/plugins/images/models.py +28 -21
@@ 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
M src/wiki/plugins/images/settings.py => src/wiki/plugins/images/settings.py +18 -19
@@ 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
+)
M src/wiki/plugins/images/templatetags/wiki_images_tags.py => src/wiki/plugins/images/templatetags/wiki_images_tags.py +2 -2
@@ 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
M src/wiki/plugins/images/views.py => src/wiki/plugins/images/views.py +64 -52
@@ 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)
M src/wiki/plugins/images/wiki_plugin.py => src/wiki/plugins/images/wiki_plugin.py +49 -38
@@ 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()]
M src/wiki/plugins/links/__init__.py => src/wiki/plugins/links/__init__.py +1 -1
@@ 1,1 1,1 @@
-default_app_config = 'wiki.plugins.links.apps.LinksConfig'
+default_app_config = "wiki.plugins.links.apps.LinksConfig"
M src/wiki/plugins/links/apps.py => src/wiki/plugins/links/apps.py +2 -2
@@ 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"
M src/wiki/plugins/links/mdx/djangowikilinks.py => src/wiki/plugins/links/mdx/djangowikilinks.py +27 -32
@@ 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
M src/wiki/plugins/links/mdx/urlize.py => src/wiki/plugins/links/mdx/urlize.py +37 -42
@@ 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):
M src/wiki/plugins/links/settings.py => src/wiki/plugins/links/settings.py +1 -1
@@ 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)
M src/wiki/plugins/links/views.py => src/wiki/plugins/links/views.py +12 -10
@@ 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)
M src/wiki/plugins/links/wiki_plugin.py => src/wiki/plugins/links/wiki_plugin.py +21 -17
@@ 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)
M src/wiki/plugins/macros/__init__.py => src/wiki/plugins/macros/__init__.py +1 -1
@@ 1,1 1,1 @@
-default_app_config = 'wiki.plugins.macros.apps.MacrosConfig'
+default_app_config = "wiki.plugins.macros.apps.MacrosConfig"
M src/wiki/plugins/macros/apps.py => src/wiki/plugins/macros/apps.py +2 -2
@@ 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"
M src/wiki/plugins/macros/mdx/macro.py => src/wiki/plugins/macros/mdx/macro.py +29 -25
@@ 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):
M src/wiki/plugins/macros/mdx/toc.py => src/wiki/plugins/macros/mdx/toc.py +2 -3
@@ 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)
M src/wiki/plugins/macros/mdx/wikilinks.py => src/wiki/plugins/macros/mdx/wikilinks.py +14 -16
@@ 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)
M src/wiki/plugins/macros/settings.py => src/wiki/plugins/macros/settings.py +3 -8
@@ 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",))
M src/wiki/plugins/macros/templatetags/wiki_macro_tags.py => src/wiki/plugins/macros/templatetags/wiki_macro_tags.py +3 -4
@@ 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
M src/wiki/plugins/macros/wiki_plugin.py => src/wiki/plugins/macros/wiki_plugin.py +10 -8
@@ 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",
]
M src/wiki/plugins/notifications/__init__.py => src/wiki/plugins/notifications/__init__.py +1 -1
@@ 1,1 1,1 @@
-default_app_config = 'wiki.plugins.notifications.apps.NotificationsConfig'
+default_app_config = "wiki.plugins.notifications.apps.NotificationsConfig"
M src/wiki/plugins/notifications/apps.py => src/wiki/plugins/notifications/apps.py +17 -13
@@ 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"]
)
M src/wiki/plugins/notifications/forms.py => src/wiki/plugins/notifications/forms.py +63 -75
@@ 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:
M src/wiki/plugins/notifications/management/commands/wiki_notifications_create_defaults.py => src/wiki/plugins/notifications/management/commands/wiki_notifications_create_defaults.py +23 -18
@@ 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",
+ )
M src/wiki/plugins/notifications/migrations/0001_initial.py => src/wiki/plugins/notifications/migrations/0001_initial.py +24 -10
@@ 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")]),
),
]
M src/wiki/plugins/notifications/migrations/0002_auto_20151118_1811.py => src/wiki/plugins/notifications/migrations/0002_auto_20151118_1811.py +2 -3
@@ 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",
),
]
M src/wiki/plugins/notifications/models.py => src/wiki/plugins/notifications/models.py +21 -21
@@ 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
M src/wiki/plugins/notifications/settings.py => src/wiki/plugins/notifications/settings.py +1 -2
@@ 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"
M src/wiki/plugins/notifications/views.py => src/wiki/plugins/notifications/views.py +25 -19
@@ 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
M src/wiki/plugins/notifications/wiki_plugin.py => src/wiki/plugins/notifications/wiki_plugin.py +10 -4
@@ 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)
M src/wiki/plugins/redlinks/__init__.py => src/wiki/plugins/redlinks/__init__.py +1 -1
@@ 1,1 1,1 @@
-default_app_config = 'wiki.plugins.redlinks.apps.RedlinksConfig'
+default_app_config = "wiki.plugins.redlinks.apps.RedlinksConfig"
M src/wiki/plugins/redlinks/apps.py => src/wiki/plugins/redlinks/apps.py +2 -2
@@ 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"
M src/wiki/plugins/redlinks/mdx/redlinks.py => src/wiki/plugins/redlinks/mdx/redlinks.py +1 -1
@@ 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):
M src/wiki/plugins/redlinks/wiki_plugin.py => src/wiki/plugins/redlinks/wiki_plugin.py +1 -1
@@ 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",
]
M src/wiki/sites.py => src/wiki/sites.py +162 -70
@@ 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()
M src/wiki/static/wiki/bootstrap/css/wiki-bootstrap.min.css => src/wiki/static/wiki/bootstrap/css/wiki-bootstrap.min.css +1 -1
@@ 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}
M src/wiki/templatetags/wiki_tags.py => src/wiki/templatetags/wiki_tags.py +30 -30
@@ 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
M src/wiki/urls.py => src/wiki/urls.py +163 -98
@@ 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()
M src/wiki/views/accounts.py => src/wiki/views/accounts.py +20 -17
@@ 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):
M src/wiki/views/article.py => src/wiki/views/article.py +249 -206
@@ 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"
M src/wiki/views/deleted_list.py => src/wiki/views/deleted_list.py +2 -2
@@ 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)
M src/wiki/views/mixins.py => src/wiki/views/mixins.py +14 -13
@@ 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
M testproject/testproject/settings/base.py => testproject/testproject/settings/base.py +47 -55
@@ 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
M testproject/testproject/settings/codehilite.py => testproject/testproject/settings/codehilite.py +2 -7
@@ 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",]
+}
M testproject/testproject/settings/customauthuser.py => testproject/testproject/settings/customauthuser.py +5 -5
@@ 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"
M testproject/testproject/settings/dev.py => testproject/testproject/settings/dev.py +7 -6
@@ 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
M testproject/testproject/settings/sendfile.py => testproject/testproject/settings/sendfile.py +2 -2
@@ 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
M testproject/testproject/urls.py => testproject/testproject/urls.py +11 -7
@@ 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"
M testproject/testproject/views.py => testproject/testproject/views.py +15 -15
@@ 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
M tests/base.py => tests/base.py +14 -14
@@ 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)
M tests/core/test_accounts.py => tests/core/test_accounts.py +37 -38
@@ 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"))
M tests/core/test_basic.py => tests/core/test_basic.py +12 -8
@@ 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()
M tests/core/test_checks.py => tests/core/test_checks.py +37 -18
@@ 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])
M tests/core/test_commands.py => tests/core/test_commands.py +4 -4
@@ 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)
M tests/core/test_forms.py => tests/core/test_forms.py +10 -6
@@ 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."],
)
M tests/core/test_managers.py => tests/core/test_managers.py +12 -39
@@ 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()])
M tests/core/test_markdown.py => tests/core/test_markdown.py +53 -62
@@ 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,
)
M tests/core/test_models.py => tests/core/test_models.py +17 -26
@@ 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)
M tests/core/test_sites.py => tests/core/test_sites.py +31 -32
@@ 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/")
M tests/core/test_template_filters.py => tests/core/test_template_filters.py +101 -93
@@ 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)
M tests/core/test_template_tags.py => tests/core/test_template_tags.py +78 -79
@@ 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)
M tests/core/test_urls.py => tests/core/test_urls.py +17 -19
@@ 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)
M tests/core/test_utils.py => tests/core/test_utils.py +0 -1
@@ 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
M tests/core/test_views.py => tests/core/test_views.py +350 -390
@@ 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")
M tests/plugins/attachments/test_commands.py => tests/plugins/attachments/test_commands.py +4 -7
@@ 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):
M tests/plugins/attachments/test_models.py => tests/plugins/attachments/test_models.py +5 -7
@@ 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,)
+ )
M tests/plugins/attachments/test_views.py => tests/plugins/attachments/test_views.py +43 -50
@@ 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):
M tests/plugins/editsection/test_editsection.py => tests/plugins/editsection/test_editsection.py +55 -44
@@ 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)
M tests/plugins/globalhistory/test_globalhistory.py => tests/plugins/globalhistory/test_globalhistory.py +41 -39
@@ 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)
M tests/plugins/images/test_forms.py => tests/plugins/images/test_forms.py +3 -3
@@ 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)
M tests/plugins/images/test_views.py => tests/plugins/images/test_views.py +81 -61
@@ 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))
M tests/plugins/links/test_links.py => tests/plugins/links/test_links.py +15 -16
@@ 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>',
M tests/plugins/links/test_urlize.py => tests/plugins/links/test_urlize.py +118 -154
@@ 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():
M tests/plugins/macros/test_links.py => tests/plugins/macros/test_links.py +3 -2
@@ 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>',
)
M tests/plugins/macros/test_toc.py => tests/plugins/macros/test_toc.py +8 -10
@@ 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)
M tests/plugins/notifications/test_forms.py => tests/plugins/notifications/test_forms.py +0 -1
@@ 8,4 8,3 @@ from wiki.plugins.notifications.forms import SettingsFormSet
class SettingsFormTests(RequireSuperuserMixin, TestCase):
def test_formset(self):
formset = SettingsFormSet(user=self.superuser1)
-
M tests/plugins/notifications/test_views.py => tests/plugins/notifications/test_views.py +32 -21
@@ 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
M tests/settings.py => tests/settings.py +36 -40
@@ 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": ""})
M tests/testdata/migrations/0001_initial.py => tests/testdata/migrations/0001_initial.py +137 -32
@@ 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()),],
),
]
M tests/testdata/models.py => tests/testdata/models.py +1 -1
@@ 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"
M tests/testdata/urls.py => tests/testdata/urls.py +10 -9
@@ 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")),
]