diff --git a/quokka/admin/ajax.py b/quokka/admin/ajax.py
deleted file mode 100644
index b223a3572..000000000
--- a/quokka/admin/ajax.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# coding: utf-8
-# TODO: adapt to tinymongo
-
-# from flask_admin.contrib.mongoengine.ajax import QueryAjaxModelLoader
-# from flask_admin.model.ajax import DEFAULT_PAGE_SIZE
-
-
-# class AjaxModelLoader(object): # QueryAjaxModelLoader):
-# """
-# """
-# def __init__(self, name, model, **options):
-# self.filters = options.pop('filters', None)
-# super(AjaxModelLoader, self).__init__(name, model, **options)
-
-# def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE):
-# query = self.model.objects
-
-# criteria = None
-
-# for field in self._cached_fields:
-# flt = {u'%s__icontains' % field.name: term}
-
-# if not criteria:
-# criteria = mongoengine.Q(**flt)
-# else:
-# criteria |= mongoengine.Q(**flt)
-
-# query = query.filter(criteria)
-
-# if self.filters:
-# query = query.filter(**self.filters)
-
-# if offset:
-# query = query.skip(offset)
-
-# return query.limit(limit).all()
diff --git a/quokka/admin/formatters.py b/quokka/admin/formatters.py
deleted file mode 100644
index 0f5b6b3f0..000000000
--- a/quokka/admin/formatters.py
+++ /dev/null
@@ -1,87 +0,0 @@
-from flask import current_app as app
-from flask_htmlbuilder.htmlbuilder import html
-from quokka.core.content.models import make_model
-
-
-def format_datetime(self, request, obj, fieldname, *args, **kwargs):
- """Returns the formated datetime in string from object"""
- model = make_model(obj)
- return html.div(style='min-width:130px;')(
- getattr(model, fieldname).strftime(
- app.config.get('ADMIN_DATE_FORMAT', '%Y-%m-%d')
- )
- )
-
-
-def format_view_on_site(self, request, obj, fieldname, *args, **kwargs):
- """Returns button to view or preview depending on content status"""
- model = make_model(obj)
- return html.a(
- href=model.external_url,
- target='_blank',
- )(html.i(class_="icon fa fa-globe glyphicon glyphicon-globe",
- style="margin-right: 5px;")(),
- 'View' if model.published else 'Preview')
-
-
-def format_ul(self, request, obj, fieldname, *args, **kwars):
- """Given a list of data format it is ul/li"""
- model = make_model(obj)
- field = getattr(model, fieldname)
- column_formatters_args = getattr(self, 'column_formatters_args', {})
- _args = column_formatters_args.get('ul', {}).get(fieldname, {})
- ul = html.ul(style=_args.get('style', "min-width:200px;max-width:300px;"))
- placeholder = _args.get('placeholder', u"{i}")
- lis = [html.li(placeholder.format(item=item)) for item in field]
- return ul(*lis)
-
-
-def format_link(self, request, obj, fieldname, *args, **kwars):
- """Format a link from the model"""
- model = make_model(obj)
- value = getattr(model, fieldname)
- return html.a(href=value, title=value, target='_blank')(
- html.i(class_="icon icon-resize-small",
- style="margin-right: 5px;")()
- )
-
-
-def format_status(self, request, obj, fieldname, *args, **kwargs):
- """Format the status published or not published and other booleans"""
- model = make_model(obj)
- status = getattr(model, fieldname)
- column_formatters_args = getattr(self, 'column_formatters_args', {})
- _args = column_formatters_args.get('status', {}).get(fieldname, {})
- labels = _args.get('labels', {})
- return html.span(
- class_="label label-{0}".format(labels.get(status, 'default')),
- style=_args.get('style', 'min-height:18px;')
- )(status)
-
-
-def format_url(self, request, obj, fieldname, *args, **kwargs):
- """Get the url of a content object"""
- column_formatters_args = getattr(self, 'column_formatters_args', {})
- _args = column_formatters_args.get('get_url', {}).get(fieldname, {})
- attribute = _args.get('attribute', 'url')
- method = _args.get('method', 'url')
- model = make_model(obj)
- text = getattr(model, fieldname, '')
- if attribute:
- target = getattr(model, attribute, None)
- else:
- target = model
-
- url = getattr(target, method, lambda: '#')()
-
- return html.a(href=url)(text if text not in [None, 'None'] else '')
-
-
-def format_custom_vars(self, request, obj, fieldname, *args, **kwargs):
- ul = html.ul(style="min-width:200px;max-width:300px;")
- lis = [
- html.li(
- html.strong(f'{item["key"]}:'), f' {item["value"]}'
- ) for item in obj.get('custom_vars', [])
- ]
- return ul(*lis)
diff --git a/quokka/admin/forms.py b/quokka/admin/forms.py
deleted file mode 100644
index a782d8d97..000000000
--- a/quokka/admin/forms.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# coding: utf-8
-
-from flask_admin.babel import Translations
-from flask_admin.form import rules # noqa
-from flask_admin.form.fields import (DateTimeField, JSONField, Select2Field,
- Select2TagsField, TimeField)
-from flask_admin.form.widgets import Select2TagsWidget
-from flask_admin.model.fields import InlineFieldList, InlineFormField
-from flask_wtf import FlaskForm
-from quokka.admin.fields import SmartSelect2Field
-from quokka.admin.wtforms_html5 import AutoAttrMeta
-from wtforms import fields as _fields
-from wtforms import widgets as _widgets
-from wtforms import validators # noqa
-from wtforms.validators import ValidationError
-
-# from wtforms_components import read_only # noqa
-# from wtforms_components import ReadOnlyWidgetProxy # noqa
-
-
-class PassiveField(object):
- """
- Passive field that does not populate obj values.
- """
- def populate_obj(self, obj, name):
- pass
-
-
-class PassiveHiddenField(PassiveField, _fields.HiddenField):
- pass
-
-
-class PassiveStringField(PassiveField, _fields.StringField):
- pass
-
-
-fields = _fields # noqa
-fields.SmartSelect2Field = SmartSelect2Field
-fields.DateTimeField = DateTimeField
-fields.TimeField = TimeField
-fields.Select2Field = Select2Field
-fields.Select2TagsField = Select2TagsField
-fields.JSONField = JSONField
-fields.InlineFieldList = InlineFieldList
-fields.InlineFormField = InlineFormField
-fields.PassiveHiddenField = PassiveHiddenField
-fields.PassiveStringField = PassiveStringField
-
-widgets = _widgets
-widgets.Select2TagsWidget = Select2TagsWidget
-
-
-READ_ONLY = {'readonly': True}
-
-
-class Form(FlaskForm):
- """Base class to customize wtforms"""
- _translations = Translations()
- Meta = AutoAttrMeta
-
- def _get_translations(self):
- return self._translations
-
-
-class CallableValidator(object):
- """
- Takes a callable and validates using it
- """
- def __init__(self, function, message=None):
- self.function = function
- self.message = message
-
- def __call__(self, form, field):
- validation = self.function(form, field)
- if validation is not None:
- raise ValidationError(self.message or validation)
-
-
-validators.CallableValidator = CallableValidator
-
-rules.csrf_token = rules.Field(
- 'csrf_token',
- render_field='quokka_macros.render_hidden_field'
-)
diff --git a/quokka/admin/widgets.py b/quokka/admin/widgets.py
deleted file mode 100644
index b03e2bddc..000000000
--- a/quokka/admin/widgets.py
+++ /dev/null
@@ -1,50 +0,0 @@
-
-from flask import current_app
-from quokka.core.template import render_template
-from wtforms.widgets import TextArea, TextInput
-
-
-class TextEditor(TextArea):
- def __init__(self, *args, **kwargs):
- super(TextEditor, self).__init__()
- self.rows = kwargs.get('rows', 20)
- self.cols = kwargs.get('cols', 20)
- self.css_cls = kwargs.get('css_cls', 'text_editor')
- self.style_ = kwargs.get(
- 'style_',
- "margin: 0px; width: 725px; height: 360px;"
- )
- self.editor = kwargs.pop('editor', 'texteditor')
-
- def __call__(self, field, **kwargs):
- c = kwargs.pop('class', '') or kwargs.pop('class_', '')
- kwargs['class'] = u'%s %s' % (self.css_cls, c)
- kwargs['rows'] = self.rows
- kwargs['cols'] = self.cols
- s = kwargs.pop('style', '') or kwargs.pop('style_', '')
- kwargs['style'] = u"%s %s" % (self.style_, s)
- html = super(TextEditor, self).__call__(field, **kwargs)
- html += render_template(
- 'admin/texteditor/%s.html' % self.editor,
- theme=current_app.config.get('ADMIN_THEME', 'admin'),
- selector='.' + self.css_cls
- )
- return html
-
-
-class PrepopulatedText(TextInput):
- def __init__(self, *args, **kwargs):
- self.master = kwargs.pop('master', '')
- super(PrepopulatedText, self).__init__(*args, **kwargs)
-
- def __call__(self, *args, **kwargs):
- html = super(PrepopulatedText, self).__call__(*args, **kwargs)
- slave = args[0].id
- if self.master:
- html += render_template(
- 'admin/custom/prepopulated.html',
- theme=current_app.config.get('ADMIN_THEME', 'admin'),
- master=self.master,
- slave=slave
- )
- return html
diff --git a/quokka/admin/wtforms_html5.py b/quokka/admin/wtforms_html5.py
deleted file mode 100644
index ccc3a1c09..000000000
--- a/quokka/admin/wtforms_html5.py
+++ /dev/null
@@ -1,279 +0,0 @@
-# -*- coding: UTF-8 -*-
-
-"""
-Generates render keywords for `WTForms`_ HTML5 field's widgets.
-
-The :func:`get_html5_kwargs` adds the automatically generated keys to the
-*render keywords* of a `WTForms`_ field.
-
-The :cls:`AutoAttrMeta` can be included as a base class for the `Meta` class
-in your forms, to handle this automatically for each field of the form.
-
-
-Supported Auto–Attributes
--------------------------
-
-- *required*
-
- Is set if the field has the _required_ flag set. This happens i.e. if you
- use the _DataRequired_ or _InputRequired_ validator. The `required`
- attribute is used by browsers to indicate a required field (and most
- browsers won't activate the forms action unless all required fields have
- content).
-
-- *invalid*
-
- If the field got any validation errors, the CSS class _invalid_ is added.
- The `invalid` class is also set by browsers if they detect errors on a
- field. This validation errors detected by your code, are by default styled
- in the same way as browser generated errors.
-
-- *min* and *max*
-
- If either _Length_ or _NumberRange_ is used as a validator to set minimal
- and / or maximal values, the corresponding `min` / `max` INPUT attribute is
- set. This allows for browser based validation of the values.
-
-- *title*
-
- If no _title_ is provided for a field, the _description_ (if one is set) is
- used for the `title` attribute.
-
-
-An Example
-----------
-
-Declare your form just like in vanilla *WTForms*, but include `AutoAttrMeta`
-as your meta class:
-
->>> from wtforms import Form, StringField
->>> from wtforms.validators import InputRequired, Length
->>> from wtforms_html5 import AutoAttrMeta
-
->>> class MyForm(Form):
-... class Meta(AutoAttrMeta):
-... pass
-... test_field = StringField(
-... 'Testfield',
-... validators=[InputRequired(), Length(min=3, max=12)],
-... description='Just a test field.',
-... )
-
->>> form = MyForm()
-
-**The only difference is, that you include a `Meta` class, that inherits from
-`AutoAttrMeta`.**
-
-This meta class sets the above mentioned attributes automatically for all the
-fields of the form:
-
->>> f = form.test_field()
->>> exp = (
-... ''
-... )
->>> assert f == exp
-
-
-
-The _min_ and _max_ attributes are created because the `Length` validator was
-used. And the field is marked _required_ because of the `InputRequired`
-validator. The field also gets a _title_ taken from the fields `description`.
-
-If you validate the form and any errors pop up, the field also get `invalid`
-added to its class:
-
->>> form.validate()
-False
->>> exp = (
-... ''
-... )
->>> f = form.test_field()
->>> assert f == exp
-
-
-.. _WTForms: https://wtforms.readthedocs.io/
-
-"""
-
-from __future__ import absolute_import, unicode_literals
-
-from wtforms.fields.core import UnboundField
-from wtforms.meta import DefaultMeta
-from wtforms.validators import Length, NumberRange
-
-__version__ = '0.3.0'
-__author__ = 'Brutus [DMC] '
-__license__ = 'GNU General Public License v3 or above - '\
- 'http://www.opensource.org/licenses/gpl-3.0.html'
-
-
-MINMAX_VALIDATORS = (
- Length,
- NumberRange,
-)
-
-
-def set_required(field, render_kw=None, force=False):
- """
- Returns *render_kw* with *required* set if the field is required.
-
- Sets the *required* key if the `required` flag is set for the field (this
- is mostly the case if it is set by validators). The `required` attribute
- is used by browsers to indicate a required field.
-
- ..note::
-
- This won't change keys already present unless *force* is used.
-
- """
- if render_kw is None:
- render_kw = {}
- if 'required' in render_kw and not force:
- return render_kw
- if field.flags.required:
- render_kw['required'] = True
- return render_kw
-
-
-def set_invalid(field, render_kw=None):
- """
- Returns *render_kw* with `invalid` added to *class* on validation errors.
-
- Set (or appends) 'invalid' to the fields CSS class(es), if the *field* got
- any errors. 'invalid' is also set by browsers if they detect errors on a
- field.
-
- """
- if render_kw is None:
- render_kw = {}
- if field.errors:
- classes = render_kw.get('class') or render_kw.pop('class_', '')
- if classes:
- render_kw['class'] = 'invalid {}'.format(classes)
- else:
- render_kw['class'] = 'invalid'
- return render_kw
-
-
-def set_minmax(field, render_kw=None, force=False):
- """
- Returns *render_kw* with *min* and *max* set if validators use them.
-
- Sets *min* and / or *max* keys if a `Length` or `NumberRange` validator is
- using them.
-
- ..note::
-
- This won't change keys already present unless *force* is used.
-
- """
- if render_kw is None:
- render_kw = {}
- for validator in field.validators:
- if isinstance(validator, MINMAX_VALIDATORS):
- if 'min' not in render_kw or force:
- v_min = getattr(validator, 'min', -1)
- if v_min not in (-1, None):
- render_kw['min'] = v_min
- if 'max' not in render_kw or force:
- v_max = getattr(validator, 'max', -1)
- if v_max not in (-1, None):
- render_kw['max'] = v_max
- return render_kw
-
-
-def set_title(field, render_kw=None):
- """
- Returns *render_kw* with *min* and *max* set if required.
-
- If the field got a *description* but no *title* key is set, the *title* is
- set to *description*.
-
- """
- if render_kw is None:
- render_kw = {}
- if 'title' not in render_kw and getattr(field, 'description'):
- render_kw['title'] = '{}'.format(field.description)
- return render_kw
-
-
-def get_html5_kwargs(field, render_kw=None, force=False):
- """
- Returns a copy of *render_kw* with keys added for a bound *field*.
-
- If some *render_kw* are given, the new keys are added to a copy of them,
- which is then returned. If none are given, a dictionary containing only
- the automatically generated keys is returned.
-
- .. important::
-
- This might add new keys but won't changes any values if a key is
- already in *render_kw*, unless *force* is used.
-
- Raises:
-
- ValueError: if *field* is an :cls:`UnboundField`.
-
- The following keys are set automatically:
-
- :required:
- Sets the *required* key if the `required` flag is set for the
- field (this is mostly the case if it is set by validators). The
- `required` attribute is used by browsers to indicate a required field.
-
- :invalid:
- Set (or appends) 'invalid' to the fields CSS class(es), if the *field*
- got any errors. 'invalid' is also set by browsers if they detect
- errors on a field.
-
- :min / max:
- Sets *min* and / or *max* keys if a `Length` or `NumberRange`
- validator is using them.
-
- :title:
- If the field got a *description* but no *title* key is set, the
- *title* is set to *description*.
-
- """
- if isinstance(field, UnboundField):
- msg = 'This function needs a bound field not: {}'
- raise ValueError(msg.format(field))
- kwargs = render_kw.copy() if render_kw else {}
- kwargs = set_required(field, kwargs, force) # is field required?
- kwargs = set_invalid(field, kwargs) # is field invalid?
- kwargs = set_minmax(field, kwargs, force) # check validators for min/max
- kwargs = set_title(field, kwargs) # missing tile?
- return kwargs
-
-
-class AutoAttrMeta(DefaultMeta):
- """
- Meta class for WTForms :cls:`Form` classes.
-
- It uses :func:`get_html5_kwargs` to automatically add some render
- keywords for each field's widget when it gets rendered.
-
- """
-
- def render_field(self, field, render_kw):
- """
- Returns the rendered field after adding auto–attributes.
-
- Calls the field`s widget with the following kwargs:
-
- 1. the *render_kw* set on the field are used as based
- 2. and are updated with the *render_kw* arguments from the render call
- 3. this is used as an argument for a call to `get_html5_kwargs`
- 4. the return value of the call is used as final *render_kw*
-
- """
- field_kw = getattr(field, 'render_kw', None)
- if callable(field_kw):
- field_kw = field_kw(field)
- if field_kw is not None:
- render_kw = dict(field_kw, **render_kw)
- render_kw = get_html5_kwargs(field, render_kw)
- return field.widget(field, **render_kw)
diff --git a/quokka/core/content/formats.py b/quokka/core/content/formats.py
deleted file mode 100644
index 2c6292dd5..000000000
--- a/quokka/core/content/formats.py
+++ /dev/null
@@ -1,408 +0,0 @@
-import datetime as dt
-import getpass
-import json
-from .parsers import markdown
-from flask import current_app as app, Markup
-from flask_admin.helpers import get_form_data
-from flask_admin.model.fields import InlineFieldList, InlineFormField
-from quokka.admin.forms import Form, fields, rules, validators
-from werkzeug.utils import import_string
-
-
-# Utils
-
-
-def get_content_formats(instances=False):
- content_formats = app.config.get(
- 'CONTENT_FORMATS',
- {
- 'markdown': {
- 'choice_text': 'Markdown',
- 'help_text': 'Markdown text editor',
- 'content_format_class':
- 'quokka.core.content.formats.MarkdownFormat'
- }
- }
- )
- if instances:
- for _, data in content_formats:
- data['content_format_instance'] = import_string(
- data['content_format_class']
- )()
- return content_formats
-
-
-def get_content_format_choices():
- content_formats = get_content_formats()
- return [
- # ('value', 'TEXT')
- (identifier, data['choice_text'])
- for identifier, data
- in content_formats.items()
- ]
-
-
-def get_format(obj):
- content_formats = get_content_formats()
- try:
- obj_content_format = content_formats[obj['content_format']]
- content_format = import_string(
- obj_content_format['content_format_class']
- )()
- return content_format
- except (KeyError):
- return PlainFormat()
-
-
-def get_edit_form(obj):
- return get_format(obj).get_edit_form(obj)
-
-
-def validate_category(form, field):
- denied_categories = app.config.get(
- 'DENIED_CATEGORIES',
- ['tag', 'tags', 'categories', 'author', 'authors', 'user',
- 'index', 'feed', 'admin', 'adm', 'login', 'logout', 'sitemap',
- 'block']
- )
- if field.data is not None:
- items = field.data.split(',')
- if len(items) > 1:
- return 'You can select only one category'
- for item in items:
- root_name = item.split('/')[0]
- if root_name in denied_categories or root_name.startswith('@'):
- return f'You cannot use `{root_name}` as a category start name'
-
-
-def get_category_kw(field):
- categories = list(app.db.category_set(sort=False))
- categories.extend(app.config.get('CATEGORIES', []))
- categories = sorted(list(set(categories)))
- return {'data-tags': json.dumps(categories),
- 'data-placeholder': 'One category or leave blank'}
-
-
-def validate_block_item(form, field):
- if field.data is not None:
- items = field.data.split(',')
- if len(items) > 1:
- return 'You can select only one URL for each item'
-
-
-def get_block_item_kw(field):
- items = [
- f"{d['content_type']}::{d['title']}::{d['category']}/{d['slug']}"
- for d in app.db.content_set()
- if d['title'] not in app.theme_context.get('TEXTBLOCKS', [])
- ]
- index = app.theme_context.get('INDEX_CATEGORY')
- items.append(f"category::{index}")
- items.extend([
- f"category::{category}"
- for category in app.db.category_set() if category
- ])
- items.extend([
- f"tag::{tag}" for tag in app.db.tag_set()
- ])
- items.extend([
- f"author::{author}" for author in app.db.author_set()
- ])
- items.extend([
- f"url::{item[0]}::{item[1]}"
- for item in app.config.get('INTERNAL_URLS', [])
- ])
- items.extend([
- f"url::category_feed_{category}::{category}/index.{ext}"
- for ext in ['rss', 'atom']
- for category in app.db.category_set() if category
- ])
- items.extend([
- f"url::tag_feed_{tag}::{tag}/index.{ext}"
- for ext in ['rss', 'atom']
- for tag in app.db.tag_set()
- ])
- block_items = sorted(list(set(items)))
- return {'data-tags': json.dumps(block_items),
- 'data-placeholder': 'Start typing, select existing or add new URL'}
-
-
-def get_default_category():
- return app.config.get('DEFAULT_CATEGORY')
-
-
-def get_authors_kw(field):
- authors = app.db.author_set(sort=False)
- authors.extend(app.config.get('AUTHORS', []))
- authors.append(getpass.getuser())
- authors = sorted(list(set(authors)))
- return {'data-tags': json.dumps(authors),
- 'data-placeholder':
- 'Enter one or more comma separated author names'}
-
-
-def get_default_author():
- authors = app.config.get('AUTHORS')
- return authors[0] if authors else getpass.getuser()
-
-
-def get_tags_kw(field):
- tags = app.db.tag_set(sort=False)
- tags.extend(app.config.get('TAGS', []))
- tags = sorted(list(set(tags)))
- return {'data-tags': json.dumps(tags),
- 'data-placeholder': 'Comma separated tags'}
-
-
-def get_default_language():
- return app.config.get('BABEL_DEFAULT_LOCALE', 'en')
-
-# classes
-
-
-class BaseForm(Form):
-
- title = fields.StringField(
- 'Title', [validators.required()],
- description='TIP: `My Title` turns to`my-title.html` url'
- )
- summary = fields.TextAreaField('Summary')
- category = fields.Select2TagsField(
- 'Category',
- [validators.CallableValidator(validate_category)],
- save_as_list=False,
- render_kw=get_category_kw,
- default=get_default_category,
- description=(
- 'TIP: Leave blank and url will be `/my-title.html`
'
- '`foo` url will be `/foo/my-title.html`
'
- '`foo/bar` url will be `/foo/bar/my-title.html`
'
- )
- )
- authors = fields.Select2TagsField(
- 'Authors',
- [validators.required()],
- save_as_list=True,
- render_kw=get_authors_kw,
- default=get_default_author
- )
-
-
-class CreateForm(BaseForm):
- """Default create form where content format is chosen"""
- # TODO: Make content_type an optional field by ASK_CONTENT_TYPE config
- # content_type = fields.SelectField(
- # 'Type',
- # [validators.required()],
- # choices=[('article', 'Article'), ('page', 'Page')]
- # )
- content_format = fields.SmartSelect2Field(
- 'Format',
- [validators.required()],
- choices=get_content_format_choices,
- # TODO: remove this `allow_blank` once select2 submit on enter is fix
- allow_blank=True
- )
-
-
-class CustomVariablesForm(Form):
- key = fields.StringField(
- 'Key', [validators.required()],
- description='lower_snake_case'
- )
- value = fields.StringField(
- 'Value', [validators.required()],
- description=(
- 'Optionally define format using @int,@float,@bool,@json '
- 'ex:`@float 42.1` or `@int 42` or `@bool false` '
- 'or `@json ["item1", "item2"]`'
- )
- )
-
-
-class BlockItemForm(Form):
- item = fields.Select2TagsField(
- 'Item',
- [validators.required(),
- validators.CallableValidator(validate_block_item)],
- save_as_list=False,
- render_kw=get_block_item_kw,
- description=(
- 'Enter absolute URL `http://..` or `/foo/bar.html` '
- 'or select existing content.'
- )
- )
- name = fields.StringField('Name', description='optional')
- order = fields.IntegerField('Order', default=0)
- item_type = fields.SmartSelect2Field(
- 'Type',
- [validators.required()],
- default='link',
- choices=lambda: [
- item for item in
- app.config.get('BLOCK_ITEM_TYPES', [('link', 'Link')])
- ]
- )
-
- custom_vars = InlineFieldList(
- InlineFormField(CustomVariablesForm), label='Custom Variables'
- )
-
- index_id = fields.HiddenField('index_id')
- category_id = fields.HiddenField('category_id')
- tag_id = fields.HiddenField('tag_id')
- author_id = fields.HiddenField('author_id')
- url_id = fields.HiddenField('url_id')
- content_type = fields.HiddenField('content_type', default='block_item')
-
-
-class BaseEditForm(BaseForm):
- """Edit form with all missing fields except `content`"""
-
- # content_type = fields.PassiveStringField(
- # 'Type',
- # render_kw=READ_ONLY
- # )
- # content_format = fields.PassiveStringField(
- # 'Format',
- # render_kw=READ_ONLY
- # )
-
- tags = fields.Select2TagsField(
- 'Tags',
- save_as_list=True,
- render_kw=get_tags_kw
- )
- date = fields.DateTimeField(
- 'Date',
- [validators.required()],
- default=dt.datetime.now
- )
- modified = fields.HiddenField('Modified')
- slug = fields.StringField('Slug')
- language = fields.SmartSelect2Field(
- 'Language',
- choices=lambda: [
- (lng, lng)
- for lng in app.config.get('BABEL_LANGUAGES', ['en'])
- ],
- default=get_default_language
- )
- # translations = fields.HiddenField('Translations')
- # todo: ^ create action 'add translation'
- published = fields.BooleanField(
- 'Status',
- render_kw={
- 'data-toggle': "toggle",
- 'data-on': "Published",
- 'data-off': "Draft",
- "data-onstyle": 'success'
- }
- )
- comments = fields.BooleanField(
- 'Comments',
- default=True,
- render_kw={
- 'data-toggle': "toggle",
- 'data-on': "Enabled",
- 'data-off': "Disabled",
- "data-onstyle": 'success'
- }
- )
-
- # to be used only for Block type
- block_items = InlineFieldList(
- InlineFormField(BlockItemForm), label='Items'
- )
-
- custom_vars = InlineFieldList(
- InlineFormField(CustomVariablesForm), label='Custom Variables'
- )
-
-
-class BaseFormat(object):
- identifier = None
- edit_form = BaseEditForm
- form_edit_rules = [
- rules.FieldSet(('title', 'summary')),
- rules.Field('content'),
- rules.FieldSet(('category', 'authors', 'tags')),
- rules.FieldSet(('date',)),
- rules.FieldSet(('slug',)),
- rules.Field('published'),
- rules.Field('comments'),
- rules.Field('custom_vars'),
- rules.csrf_token
- ]
-
- def get_edit_form(self, obj):
- return self.edit_form(get_form_data(), **obj)
-
- def get_edit_template(self, obj):
- return 'admin/quokka/edit.html'
-
- def get_identifier(self):
- return self.identifier or self.__class__.__name__
-
- def get_form_edit_rules(self, obj):
- return self.form_edit_rules
-
- def before_save(self, form, model, is_created):
- """optional"""
-
- def after_save(self, form, model, is_created):
- """optional"""
-
- def extra_js(self):
- return []
-
- def render_content(self, obj):
-
- if not isinstance(obj, dict):
- content = obj.data
- else:
- content = obj
-
- if 'content' not in content:
- content = app.db.get_with_content(_id=content['_id'])
-
- return content['content']
-
- def render(self, obj):
- # TODO: PRE-RENDER
- rv = self.render_content(obj)
- # TODO: POST-RENDER
- return rv
-
-
-# Customs
-
-
-class PlainEditForm(BaseEditForm):
- content = fields.TextAreaField('Plain Content')
-
-
-class PlainFormat(BaseFormat):
- edit_form = PlainEditForm
-
-
-class HTMLEditForm(BaseEditForm):
- content = fields.TextAreaField('HTML Content')
-
-
-class HTMLFormat(BaseFormat):
- edit_form = HTMLEditForm
-
-
-class MarkdownEditForm(BaseEditForm):
- content = fields.TextAreaField('Markdown Content')
-
-
-class MarkdownFormat(BaseFormat):
- edit_form = MarkdownEditForm
-
- def render_content(self, obj):
- content = super().render_content(obj)
- if content:
- return Markup(markdown(content))
- return content or ''
diff --git a/quokka/core/content/models.py b/quokka/core/content/models.py
index 6d2fcc4d9..5358ed079 100644
--- a/quokka/core/content/models.py
+++ b/quokka/core/content/models.py
@@ -230,13 +230,13 @@ def metadata(self):
# TODO: get metadata from database
# TODO: implement libratar/gravatar
# return {
- # 'cover': 'foo',
- # 'author_gravatar': 'http://i.pravatar.cc/300',
- # 'about_author': 'About Author',
- # 'translations': ['en'],
- # 'og_image': 'foo',
- # 'series': 'aa',
- # 'asides': 'aaa'
+ # 'cover': 'foo',
+ # 'author_gravatar': 'http://i.pravatar.cc/300',
+ # 'about_author': 'About Author',
+ # 'translations': ['en'],
+ # 'og_image': 'foo',
+ # 'series': 'aa',
+ # 'asides': 'aaa'
# }
data = {}
data.update(custom_var_dict(self.data.get('custom_vars')))
diff --git a/quokka/core/content/utils.py b/quokka/core/content/utils.py
index 57aaef312..d4c5ff1e7 100644
--- a/quokka/core/content/utils.py
+++ b/quokka/core/content/utils.py
@@ -10,10 +10,12 @@ def url_for_content(content, include_ext=True):
else:
data = content
+ category_slug_data = data.get('category_slug')
+ category_data = slugify_category(data.get('category') or '')
category_slug = (
- data.get('category_slug') or
- slugify_category(data.get('category') or '')
+ category_slug_data or category_data
)
+
slug = data.get('slug') or slugify(data.get('title'))
if category_slug:
diff --git a/quokka/core/content/views.py b/quokka/core/content/views.py
index 74c72d288..08d6d9bc1 100644
--- a/quokka/core/content/views.py
+++ b/quokka/core/content/views.py
@@ -255,6 +255,8 @@ def render_rss(self, content_type, templates, **context):
for content in contents:
content = make_model(content)
+ content_data = content.title.encode('utf-8')
+ content_data += content.url.encode('utf-8')
if content.date > rss_pubdate:
rss_pubdate = content.date
@@ -267,8 +269,7 @@ def render_rss(self, content_type, templates, **context):
author=str(content.author),
categories=[str(content.tags)],
guid=hashlib.sha1(
- content.title.encode('utf-8') +
- content.url.encode('utf-8')
+ content_data
).hexdigest(),
pubDate=content.date,
)
diff --git a/quokka/core/db.py b/quokka/core/db.py
index 431c55aea..afc09ad77 100644
--- a/quokka/core/db.py
+++ b/quokka/core/db.py
@@ -183,17 +183,14 @@ def page_set(self, *args, **kwargs):
return self.content_set(*args, **kwargs)
def block_set(self, *args, **kwargs):
- kwargs.setdefault(
- 'sort',
- self.app.theme_context.get(
- 'BLOCK_ORDER_BY', [('title', -1)]
- )
- )
- if not args:
- args = [{'content_type': 'block'}]
- elif isinstance(args[0], dict):
- args[0]['content_type'] = 'block'
- return self.content_set(*args, **kwargs)
+ kwargs.setdefault('sort', self.app.theme_context.get(
+ 'BLOCK_ORDER_BY', [('title', -1)]
+ ))
+ if not args:
+ args = [{'content_type': 'block'}]
+ elif isinstance(args[0], dict):
+ args[0]['content_type'] = 'block'
+ return self.content_set(*args, **kwargs)
def select(self, colname, *args, **kwargs):
return self.get_collection(colname).find(*args, **kwargs)
diff --git a/quokka/core/views/sitemap.py b/quokka/core/views/sitemap.py
index 871372817..2cdca943b 100644
--- a/quokka/core/views/sitemap.py
+++ b/quokka/core/views/sitemap.py
@@ -11,12 +11,12 @@ def get_contents(self):
TODO: Should include extra paths, fixed paths
config based paths, static paths
"""
+ content = self.get_index() + self.get_categories()
+ content += self.get_tags() + self.get_authors()
+ content += self.get_articles_and_pages()
+
return (
- self.get_index() +
- self.get_categories() +
- self.get_tags() +
- self.get_authors() +
- self.get_articles_and_pages()
+ content
)
def get_index(self):
diff --git a/quokka/module_template/quokka_module_template/admin.py b/quokka/module_template/quokka_module_template/admin.py
deleted file mode 100644
index 16db05372..000000000
--- a/quokka/module_template/quokka_module_template/admin.py
+++ /dev/null
@@ -1,90 +0,0 @@
-from flask import current_app
-from flask_admin.contrib.pymongo import filters
-from flask_admin.form import Select2Widget
-from flask_admin.model.fields import InlineFieldList, InlineFormField
-from quokka.admin.forms import Form, fields
-from quokka.admin.views import ModelView
-
-
-# User admin
-class InnerForm(Form):
- username = fields.StringField('Username')
- test = fields.StringField('Test')
-
-
-class UserForm(Form):
- username = fields.StringField('Username')
- email = fields.StringField('Email')
- password = fields.StringField('Password')
-
- # Inner form
- inner = InlineFormField(InnerForm)
-
- # Form list
- form_list = InlineFieldList(InlineFormField(InnerForm))
-
-
-class UserView(ModelView):
- column_list = ('username', 'email', 'password')
- column_sortable_list = ('username', 'email', 'password')
-
- form = UserForm
-
- page_size = 20
- can_set_page_size = True
-
- # Correct user_id reference before saving
- def on_model_change(self, form, model):
- model['_id'] = model.get('username')
- return model
-
-
-# Tweet view
-class TweetForm(Form):
- name = fields.StringField('Name')
- user_id = fields.SelectField('User', widget=Select2Widget())
- text = fields.StringField('Text')
- testie = fields.BooleanField('Test')
-
-
-class TweetView(ModelView):
- column_list = ('name', 'user_name', 'text')
- column_sortable_list = ('name', 'text')
-
- column_filters = (filters.FilterEqual('name', 'Name'),
- filters.FilterNotEqual('name', 'Name'),
- filters.FilterLike('name', 'Name'),
- filters.FilterNotLike('name', 'Name'),
- filters.BooleanEqualFilter('testie', 'Testie'))
-
- # column_searchable_list = ('name', 'text')
-
- form = TweetForm
-
- def get_list(self, *args, **kwargs):
- # not necessary but kept as example
- count, data = super(TweetView, self).get_list(*args, **kwargs)
-
- # Contribute user_name to the models
- for item in data:
- user = current_app.db.users.find_one(
- {'_id': item['user_id']}
- )
- if user:
- item['user_name'] = user['_id']
-
- return count, data
-
- # Contribute list of user choices to the forms
- def _feed_user_choices(self, form):
- users = current_app.db.users.find(fields=('_id',))
- form.user_id.choices = [(str(x['_id']), x['_id']) for x in users]
- return form
-
- def create_form(self):
- form = super(TweetView, self).create_form()
- return self._feed_user_choices(form)
-
- def edit_form(self, obj):
- form = super(TweetView, self).edit_form(obj)
- return self._feed_user_choices(form)
diff --git a/quokka/utils/upload.py b/quokka/utils/upload.py
index 251e1c50f..bc8042679 100644
--- a/quokka/utils/upload.py
+++ b/quokka/utils/upload.py
@@ -9,7 +9,7 @@
def dated_path(obj, file_data):
try:
- prefix = getattr(obj, 'model_name')
+ prefix = getattr(obj, 'model_name', None)
except BaseException:
prefix = "undefined"
diff --git a/requirements-dev.txt b/requirements-dev.txt
index ba32aa2f0..7eda9b364 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -12,3 +12,4 @@ twine>=1.9.1
wheel>=0.30.0
flask>=1.0
logzero
+pytest_mock
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/quokka/__init__.py b/tests/quokka/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/quokka/project_template/__init__.py b/tests/quokka/project_template/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_basic.py b/tests/test_basic.py
index 1f9418114..aa9515152 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -1,3 +1,154 @@
+import mock
+import quokka
+from quokka import create_app, create_app_base
+from pytest_mock import mocker
+from quokka.core.app import QuokkaApp
+from quokka.core.flask_dynaconf import configure_dynaconf
+
+
+################################
+# pytest - fixtures - setUp(); #
+################################
+class MockTestApp(object):
+
+ def __init__(self, config):
+ self.config = config
+ return self.config
+
+
+#####################################
+# pytest - Quokka - test__init__.py #
+#####################################
+def test_create_app_called_params_default(mocker):
+ mocker.patch("quokka.create_app_base")
+ mocker.patch("quokka.core.configure_extensions")
+ quokka.create_app()
+ quokka.create_app_base.assert_called_once_with(test=False)
+
+
+def test_create_app_called_test_false(mocker):
+ mocker.patch("quokka.create_app_base")
+ mocker.patch("quokka.core.configure_extensions")
+ quokka.create_app(test=False)
+ quokka.create_app_base.assert_called_once_with(test=False)
+
+
+def test_create_app_called_test_true(mocker):
+ mocker.patch("quokka.create_app_base")
+ mocker.patch("quokka.core.configure_extensions")
+ quokka.create_app(test=True)
+ quokka.create_app_base.assert_called_once_with(test=True)
+
+
+def test_create_app_called_test_true_and_settings_dict(mocker):
+ mocker.patch("quokka.create_app_base")
+ mocker.patch("quokka.core.configure_extensions")
+ quokka.create_app(test=True, settings={'a':'1', 'b':'2', 'c':'3', 'd':'4', 'e':'5'})
+ quokka.create_app_base.assert_called_once_with(test=True, settings={'a':'1', 'b':'2', 'c':'3', 'd':'4', 'e':'5'})
+
+
+@mock.patch("quokka.core.app.QuokkaApp")
+@mock.patch("quokka.core.flask_dynaconf.configure_dynaconf")
+@mock.patch("quokka.core.configure_extension")
+def test_create_app_base_function_quokkaapp_called_is_false(mock_configure_extension, mock_configure_dynaconf, mock_QuokkaApp):
+ quokka.create_app_base(test=False, ext_list=None)
+ assert mock_QuokkaApp.called is False
+
+
+@mock.patch("quokka.core.app.QuokkaApp")
+@mock.patch("quokka.core.flask_dynaconf.configure_dynaconf")
+@mock.patch("quokka.core.configure_extension")
+def test_create_app_base_function_dynaconf_called_is_false(mock_configure_extension, mock_configure_dynaconf, mock_QuokkaApp):
+ quokka.create_app_base(test=False, ext_list=None)
+ assert mock_configure_dynaconf.called is False
+
+
+@mock.patch("quokka.core.app.QuokkaApp")
+@mock.patch("quokka.core.flask_dynaconf.configure_dynaconf")
+@mock.patch("quokka.core.configure_extension")
+def test_create_app_base_function_configure_extension_called_is_false(mock_configure_extension, mock_configure_dynaconf, mock_QuokkaApp):
+ quokka.create_app_base(test=False, ext_list=None)
+ assert mock_configure_extension.called is False
+
+
+@mock.patch("quokka.core.app.QuokkaApp")
+@mock.patch("quokka.core.flask_dynaconf.configure_dynaconf")
+@mock.patch("quokka.core.configure_extension")
+def test_create_app_base_function_quokkaapp_called_is_false_and_test_true(mock_configure_extension, mock_configure_dynaconf, mock_QuokkaApp):
+ quokka.create_app_base(test=True, ext_list=[])
+ assert mock_QuokkaApp.called is False
+
+
+@mock.patch("quokka.core.app.QuokkaApp")
+@mock.patch("quokka.core.flask_dynaconf.configure_dynaconf")
+@mock.patch("quokka.core.configure_extension")
+def test_create_app_base_function_dynaconf_called_is_false_test_true_and_ext_list(mock_configure_extension, mock_configure_dynaconf, mock_QuokkaApp):
+ quokka.create_app_base(test=True, ext_list=['quokka.core.configure_extension'])
+ assert mock_configure_dynaconf.called is False
+
+
+@mock.patch("quokka.core.app.QuokkaApp")
+@mock.patch("quokka.core.flask_dynaconf.configure_dynaconf")
+@mock.patch("quokka.core.configure_extension")
+def test_create_app_base_function_configure_dynaconf_called_is_true(mock_configure_extension, mock_configure_dynaconf, mock_QuokkaApp):
+ list_ext = ['quokka.core.app.QuokkaApp',
+ 'quokka.core.flask_dynaconf.configure_dynaconf',
+ 'quokka.core.configure_extension']
+ quokka.create_app_base(test=True, ext_list=list_ext)
+ assert mock_configure_dynaconf.called is True
+
+
+@mock.patch("quokka.core.app.QuokkaApp")
+@mock.patch("quokka.core.flask_dynaconf.configure_dynaconf")
+@mock.patch("quokka.core.configure_extension")
+def test_create_app_base_function_quokkaapp_called_is_true(mock_configure_extension, mock_configure_dynaconf, mock_QuokkaApp):
+ list_ext = ['quokka.core.app.QuokkaApp',
+ 'quokka.core.flask_dynaconf.configure_dynaconf',
+ 'quokka.core.configure_extension']
+ quokka.create_app_base(test=True, ext_list=list_ext)
+ assert mock_QuokkaApp.called is True
+
+
+@mock.patch("quokka.core.app.QuokkaApp")
+@mock.patch("quokka.core.flask_dynaconf.configure_dynaconf")
+@mock.patch("quokka.core.configure_extension")
+def test_create_app_base_function_configure_extension_called_is_true(mock_configure_extension, mock_configure_dynaconf, mock_QuokkaApp):
+ list_ext = ['quokka.core.app.QuokkaApp',
+ 'quokka.core.flask_dynaconf.configure_dynaconf',
+ 'quokka.core.configure_extension']
+ quokka.create_app_base(test=True, ext_list=list_ext)
+ assert mock_configure_extension.called is True
+
+
+@mock.patch("quokka.core.app.QuokkaApp")
+@mock.patch("quokka.core.flask_dynaconf.configure_dynaconf")
+@mock.patch("quokka.core.configure_extension")
+def test_create_app_base_function_configure_extension_called_is_true_and_settings(mock_configure_extension, mock_configure_dynaconf, mock_QuokkaApp):
+ list_ext = ['quokka.core.app.QuokkaApp',
+ 'quokka.core.flask_dynaconf.configure_dynaconf',
+ 'quokka.core.configure_extension']
+ quokka.create_app_base(test=True, ext_list=list_ext, settings={'a':'1', 'b':'2', 'c':'3', 'd':'4', 'e':'5'})
+ assert mock_configure_extension.called is True
+
+
+@mock.patch("quokka.core.app.QuokkaApp")
+@mock.patch("quokka.core.flask_dynaconf.configure_dynaconf")
+@mock.patch("quokka.core.configure_extension")
+def test_create_app_base_function_configure_dynaconf_called_is_true_and_settings(mock_configure_extension, mock_configure_dynaconf, mock_QuokkaApp):
+ list_ext = ['quokka.core.app.QuokkaApp',
+ 'quokka.core.flask_dynaconf.configure_dynaconf',
+ 'quokka.core.configure_extension']
+ quokka.create_app_base(test=True, ext_list=list_ext, settings={'a':'1', 'b':'2', 'c':'3', 'd':'4', 'e':'5'})
+ assert mock_configure_dynaconf.called is True
+
+
+@mock.patch("quokka.core.app.QuokkaApp")
+@mock.patch("quokka.core.flask_dynaconf.configure_dynaconf")
+@mock.patch("quokka.core.configure_extension")
+def test_create_app_base_function_quokkaapp_called_is_true_and_settings(mock_configure_extension, mock_configure_dynaconf, mock_QuokkaApp):
+ list_ext = ['quokka.core.app.QuokkaApp',
+ 'quokka.core.flask_dynaconf.configure_dynaconf',
+ 'quokka.core.configure_extension']
+ quokka.create_app_base(test=True, ext_list=list_ext, settings={'a':'1', 'b':'2', 'c':'3', 'd':'4', 'e':'5'})
+ assert mock_QuokkaApp.called is True
-def test_basic(app):
- assert app.name == 'quokka'