PK fK3:I I quokka/__init__.py"""Quokka CMS!"""
__version__ = '0.3.1'
from quokka.core.app import QuokkaApp
from quokka.core import configure_extensions, configure_extension
from quokka.core.flask_dynaconf import configure_dynaconf
def create_app_base(test=False, ext_list=None, **settings):
"""Creates basic app only with extensions provided in ext_list
useful for testing."""
app = QuokkaApp('quokka')
configure_dynaconf(app)
if settings:
app.config.update(settings)
if test or app.config.get('TESTING'):
app.testing = True
if ext_list:
for ext in ext_list:
configure_extension(ext, app=app)
return app
def create_app(test=False, **settings):
"""Creates full app with all extensions loaded"""
app = create_app_base(test=test, **settings)
configure_extensions(app)
return app
PK KgrK/
/
quokka/cli.py# coding: utf-8
import errno
import shutil
import sys
from pathlib import Path
from pprint import pprint
import click
import yaml
from manage.cli import cli, init_cli
from manage.template import default_manage_dict
from quokka.core.auth import create_user
from quokka.core.errors import DuplicateKeyError
from . import create_app as App
@cli.command()
@click.option('--reloader/--no-reloader', default=True)
@click.option('--debug/--no-debug', default=True)
@click.option('--host', default='127.0.0.1')
@click.option('--port', default=5000)
def runserver(reloader, debug, host, port):
"""Run the Flask development server i.e. app.run()"""
App().run(
use_reloader=reloader,
debug=debug,
host=host,
port=port,
extra_files=['settings.yml', '.secrets.yml'] # for reloader
)
@cli.command()
def check():
"""Prints app status"""
app = App()
click.echo("Extensions.")
pprint(app.extensions)
click.echo("Modules.")
pprint(app.blueprints)
click.echo("App.")
return app
@cli.command()
def showconfig():
"""click.echo all Quokka config variables"""
from pprint import pprint
click.echo("Config.")
pprint(dict(App().config))
def copyfolder(src, dst):
try:
shutil.copytree(src, dst)
except OSError as exc:
if exc.errno == errno.ENOTDIR:
shutil.copy(src, dst)
else:
raise
@cli.command()
@click.argument('name', required=True)
@click.option('--destiny', required=False, default=None)
@click.option('--source', required=False, default=None)
@click.option('--theme', required=False, default=None)
@click.option('--modules', required=False, default=None)
def init(name, destiny, source, theme, modules):
"""Initialize a new project in current folder\n
$ quokka init mywebsite
"""
folder_name = name.replace(' ', '_').lower()
if destiny is None:
destiny = f'./{folder_name}'
else:
destiny = f'{destiny}/{folder_name}'
source = source or Path.joinpath(
Path(sys.modules['quokka'].__file__).parent,
'project_template'
)
copyfolder(source, destiny)
# TODO: fetch theme
# TODO: fetch and install modules
with open(f'{destiny}/manage.yml', 'w') as manage_file:
data = default_manage_dict
data['project_name'] = name.title()
manage_file.write(yaml.dump(data, default_flow_style=False))
@cli.command()
@click.option('--username', required=True, prompt=True)
@click.option('--email', required=False, default=None, prompt=True)
@click.option('--password', required=True, prompt=True, hide_input=True,
confirmation_prompt=True)
def adduser(username, email, password):
"""Add new user with admin access"""
app = App()
with app.app_context():
try:
create_user(username=username, password=password, email=email)
except DuplicateKeyError as e:
click.echo(str(e).replace('_id', 'username'))
else:
click.echo(
'User {0} created!!! go to: {1}'.format(username, '/admin')
)
def main():
"""
Quokka CMS command line manager
overwrites the manage loader
"""
manager = init_cli(cli)
# TODO: implement locked: to avoid manage to run
return manager() # from quokka.utils.populate import Populate
PK IKi
quokka/admin/__init__.pyimport import_string
from flask_admin import Admin
from tinymongo import TinyMongoCollection
from .views import FileAdmin, IndexView, ModelView
class QuokkaAdmin(Admin):
"""Customizable admin"""
registered = []
def register(self, model, view=None, identifier=None, *args, **kwargs):
"""Register views in a simpler way
admin.register(
Link,
LinkAdmin,
category="Content",
name="Link"
)
"""
_view = view or ModelView
if not identifier:
if isinstance(model, TinyMongoCollection):
identifier = '.'.join(
(view.__module__, view.__name__, model.tablename)
)
else:
identifier = '.'.join((model.__module__, model.__name__))
if identifier not in self.registered:
self.add_view(_view(model, *args, **kwargs))
self.registered.append(identifier)
def create_admin(app=None):
"""Admin factory"""
index_view = IndexView()
return QuokkaAdmin(
app,
index_view=index_view,
template_mode=app.config.get('FLASK_ADMIN_TEMPLATE_MODE')
)
def configure_admin(app, admin=None): # noqa
"""Configure admin extensions"""
admin = admin or create_admin(app)
custom_index = app.config.get('ADMIN_INDEX_VIEW')
if custom_index:
admin.index_view = import_string(custom_index)()
if isinstance(admin._views[0], IndexView): # noqa
del admin._views[0] # noqa
admin._views.insert(0, admin.index_view) # noqa
admin_config = app.config.get(
'ADMIN',
{
'name': 'Quokka Admin',
'url': '/admin'
}
)
for key, value in list(admin_config.items()):
setattr(admin, key, value)
# avoid registering twice
if admin.app is None:
admin.init_app(app)
return admin
def configure_file_admin(app):
for entry in app.config.get('FILE_ADMIN', []):
try:
app.admin.add_view(
FileAdmin(
entry['path'],
entry['url'],
name=entry['name'],
category=entry['category'],
endpoint=entry['endpoint'],
editable_extensions=entry.get('editable_extensions')
)
)
except Exception as e:
app.logger.info(e)
def configure_extra_views(app):
# adding extra views
extra_views = app.config.get('ADMIN_EXTRA_VIEWS', [])
for view in extra_views:
app.admin.add_view(
import_string(view['module'])(
category=view.get('category'),
name=view.get('name')
)
)
PK Kl? ? quokka/admin/actions.py
import datetime
import json
import random
from flask import Response, flash, redirect, url_for
from flask_admin.actions import action
class PublishAction(object):
@action(
'toggle_publish',
'Publish/Unpublish',
'Publish/Unpublish?'
)
def action_toggle_publish(self, ids):
for i in ids:
instance = self.get_instance(i)
instance.published = not instance.published
instance.save()
count = len(ids)
flash(
f'{count} items were successfully published/Unpublished.',
'success'
)
class CloneAction(object):
@action(
'clone_item',
'Create a copy',
'Are you sure you want a copy?'
)
def action_clone_item(self, ids):
if len(ids) > 1:
flash(
"You can select only one item for this action",
'error'
)
return
instance = self.get_instance(ids[0])
new = instance.from_json(instance.to_json())
new.id = None
new.published = False
new.last_updated_by = None # User.objects.get(id=current_user.id)
new.updated_at = datetime.datetime.now()
new.slug = "{0}-{1}".format(new.slug, random.getrandbits(32))
new.save()
return redirect(url_for('.edit_view', id=new.id))
class ExportAction(object):
@action('export_to_json', 'Export as json')
def export_to_json(self, ids):
qs = self.model.objects(id__in=ids)
return Response(
qs.to_json(),
mimetype="text/json",
headers={
"Content-Disposition":
"attachment;filename=%s.json" % self.model.__name__.lower()
}
)
@action('export_to_csv', 'Export as csv')
def export_to_csv(self, ids):
qs = json.loads(self.model.objects(id__in=ids).to_json())
def generate():
yield ','.join(list(max(qs, key=lambda x: len(x)).keys())) + '\n'
for item in qs:
yield ','.join([str(i) for i in list(item.values())]) + '\n'
return Response(
generate(),
mimetype="text/csv",
headers={
"Content-Disposition":
"attachment;filename=%s.csv" % self.model.__name__.lower()
}
)
PK KO= quokka/admin/ajax.py# 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()
PK K%B( ( quokka/admin/fields.py# coding: utf-8
# import random
# import sys
# from flask import current_app
from flask_admin import form
from flask_admin.form.upload import ImageUploadInput
# from werkzeug.datastructures import FileStorage
class SmartSelect2Field(form.Select2Field):
def iter_choices(self):
"""
We should update how choices are iter to make sure that value from
internal list or tuple should be selected.
"""
if self.allow_blank:
yield (u'__None', self.blank_text, self.data is None)
for value, label in self.concrete_choices:
yield (value, label, self.coerce(value) == self.data)
# for value, label in self.concrete_choices:
# yield (value, label, (self.coerce, self.data))
@property
def concrete_choices(self):
if callable(self.choices):
return self.choices()
return self.choices
@property
def choice_values(self):
return [value for value, label in self.concrete_choices]
def pre_validate(self, form):
if self.allow_blank and self.data is None:
return
values = self.choice_values
if (self.data is None and u'' in values) or self.data in values:
return True
super(SmartSelect2Field, self).pre_validate(form)
class ThumbWidget(ImageUploadInput):
empty_template = ""
data_template = ('
'
' '
'
')
class ThumbField(form.ImageUploadField):
widget = ThumbWidget()
# class ImageUploadField(form.ImageUploadField):
# def is_file_allowed(self, filename):
# extensions = self.allowed_extensions # noqa
# if isinstance(extensions, (str, unicode)) and extensions.isupper():
# items = current_app.config.get(extensions, extensions)
# merged_items = [
# item.lower() for item in items
# ] + [item.upper() for item in items]
# self.allowed_extensions = merged_items
# return super(ImageUploadField, self).is_file_allowed(filename)
class ContentImageField(form.ImageUploadField):
def populate_obj(self, obj, name):
pass
# def save_contents(self, obj):
# # field = getattr(obj, name, None)
# # if field:
# # # If field should be deleted, clean it up
# # if self._should_delete:
# # self._delete_file(field)
# # setattr(obj, name, None)
# # return
# new_image = Image(
# title=u"Image: {0}".format(obj.title),
# slug=u"{0}-{1}".format(obj.slug, random.getrandbits(8)),
# channel=Image.get_default_channel(),
# published=True
# )
# if self.data and isinstance(self.data, FileStorage):
# # if field:
# # self._delete_file(field)
# filename = self.generate_name(new_image, self.data)
# filename = self._save_file(self.data, filename)
# setattr(new_image, 'path', filename)
# new_image.save()
# if obj.contents.filter(identifier='mainimage'):
# purpose = SubContentPurpose.objects.get(identifier='image')
# else:
# purpose = SubContentPurpose.objects.get(
# identifier='mainimage'
# )
# subcontent = SubContent(
# content=new_image,
# purpose=purpose,
# )
# obj.contents.append(subcontent)
# obj.save()
PK ~rJ#ә quokka/admin/formatters.py
from flask_htmlbuilder.htmlbuilder import html
from quokka.admin.utils import _l
def format_datetime(self, request, obj, fieldname, *args, **kwargs):
return html.div(style='min-width:130px;')(
getattr(obj, fieldname).strftime(self.get_datetime_format())
)
def view_on_site(self, request, obj, fieldname, *args, **kwargs):
available = obj.is_available
endpoint = kwargs.pop(
'endpoint',
'quokka.core.detail' if available else 'quokka.core.preview'
)
return html.a(
href=obj.get_absolute_url(endpoint),
target='_blank',
)(html.i(class_="icon icon-eye-open", style="margin-right: 5px;")(),
_l('View on site') if available else _l('Preview on site'))
def format_ul(self, request, obj, fieldname, *args, **kwars):
field = getattr(obj, 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):
value = getattr(obj, 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):
status = getattr(obj, 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 get_url(self, request, obj, fieldname, *args, **kwargs):
column_formatters_args = getattr(self, 'column_formatters_args', {})
_args = column_formatters_args.get('get_url', {}).get(fieldname, {})
attribute = _args.get('attribute')
method = _args.get('method', 'get_absolute_url')
text = getattr(obj, fieldname, '')
if attribute:
target = getattr(obj, attribute, None)
else:
target = obj
url = getattr(target, method, lambda: '#')()
return html.a(href=url)(text if text not in [None, 'None'] else '')
PK K quokka/admin/forms.py# 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
PK 'K`g g quokka/admin/views.py# coding: utf -8
from flask import current_app, redirect
# from flask_admin.babel import gettext, ngettext
from flask_admin import AdminIndexView
from flask_admin.contrib.fileadmin import FileAdmin as _FileAdmin
from flask_admin.contrib.pymongo import ModelView as PyMongoModelView
# from flask_admin import helpers as h
from flask_login import current_user, login_url
from quokka.admin.actions import CloneAction, PublishAction
# from quokka.template import render_template
# from quokka.admin.widgets import PrepopulatedText
# from quokka.admin.fields import ContentImageField
# from quokka.utils.upload import dated_path, lazy_media_path
from quokka.utils.routing import expose
# from flask_admin import BaseView as AdminBaseView
# from quokka.admin.fields import ThumbField
# from quokka.admin import formatters
class ThemeMixin(object):
pass
# def render(self, template, **kwargs):
# # Store self as admin_view
# kwargs['admin_view'] = self
# kwargs['admin_base_template'] = self.admin.base_template
# # Provide i18n support even if flask-babel is not installed or
# enabled.
# kwargs['_gettext'] = gettext
# kwargs['_ngettext'] = ngettext
# kwargs['h'] = h
# # Contribute extra arguments
# kwargs.update(self._template_args)
# theme = current_app.config.get('ADMIN_THEME')
# return render_template(template, theme=theme, **kwargs)
# def render(self, template, **kwargs):
# """
# Render template
# :param template:
# Template path to render
# :param kwargs:
# Template arguments
# """
# # Store self as admin_view
# kwargs['admin_view'] = self
# kwargs['admin_base_template'] = self.admin.base_template
# # Provide i18n support even if flask-babel is not installed
# # or enabled.
# kwargs['_gettext'] = babel.gettext
# kwargs['_ngettext'] = babel.ngettext
# kwargs['h'] = h
# # Expose get_url helper
# kwargs['get_url'] = self.get_url
# # Expose config info
# kwargs['config'] = current_app.config
# # Contribute extra arguments
# kwargs.update(self._template_args)
# return render_template(template, **kwargs)
class RequiresLogin(object):
def _handle_view(self, *args, **kwargs): # noqa
"""Admin views requires login"""
if current_app.config.get('ADMIN_REQUIRES_LOGIN') is True:
if not current_user.is_authenticated:
return redirect(login_url('quokka.login', next_url="/admin"))
class FileAdmin(ThemeMixin, RequiresLogin, _FileAdmin):
def __init__(self, *args, **kwargs):
self.editable_extensions = kwargs.pop('editable_extensions', tuple())
super(FileAdmin, self).__init__(*args, **kwargs)
class IndexView(RequiresLogin, ThemeMixin, AdminIndexView):
@expose('/')
def index(self):
return self.render('admin/index.html')
class ModelView(CloneAction, PublishAction,
ThemeMixin, RequiresLogin,
PyMongoModelView):
"""Base model view for all contents"""
page_size = 20
can_set_page_size = True
def _get_endpoint(self, endpoint):
if not endpoint:
endpoint = self.__class__.__name__.lower()
endpoint = f'{self.__module__}.{endpoint}'
return endpoint
# def create_blueprint(self, *args, **kwargs):
# bp = super().create_blueprint(*args, **kwargs)
# bp.name = f'{self.__module__}.{bp.name}'
# # print(self.__module__)
# return bp
# form_subdocuments = {}
# datetime_format = "%Y-%m-%d %H:%M"
# formatters = {
# 'datetime': formatters.format_datetime,
# 'view_on_site': formatters.view_on_site,
# 'ul': formatters.format_ul,
# 'status': formatters.format_status,
# 'get_url': formatters.get_url,
# 'link': formatters.format_link
# }
# column_formatters_args = {}
# def get_datetime_format(self):
# return current_app.config.get('DATETIME_FORMAT',
# self.datetime_format)
# class BaseContentView(ModelView):
# """
# All attributes added here for example
# more info in admin source
# github.com/mrjoes/flask-admin/blob/master/flask_admin/model/base.py
# or Flask-admin documentation
# """
# roles_accepted = ('admin', 'editor', 'author')
# can_create = True
# can_edit = True
# can_delete = True
# # list_template = 'admin/model/list.html'
# # edit_template = 'admin/custom/edit.html'
# # create_template = 'admin/custom/create.html'
# column_list = (
# 'title', 'slug', 'channel', 'published', 'created_at',
# 'available_at', 'view_on_site'
# )
# column_formatters = {
# 'view_on_site': formatters.view_on_site,
# 'created_at': formatters.format_datetime,
# 'available_at': formatters.format_datetime
# }
# # column_type_formatters = {}
# # column_labels = {}
# # column_descriptions = {}
# # column_sortable_list = [] / ('name', ('user', 'user.username'))
# # column_default_sort = 'pk'
# # column_choices = {'column': ('value', 'display')}
# # column_display_pk = True
# column_filters = ['published', 'title', 'summary',
# 'created_at', 'available_at']
# column_searchable_list = ('title', 'summary')
# form_columns = ['title', 'slug', 'channel', 'related_channels',
# 'summary',
# 'published', 'add_image', 'contents',
# 'show_on_channel', 'available_at', 'available_until',
# 'tags', 'values', 'template_type', 'license', 'authors']
# # form_excluded_columns = []
# # form = None
# # form_overrides = None
# form_widget_args = {
# 'summary': {
# 'style': 'width: 400px; height: 100px;'
# },
# 'title': {'style': 'width: 400px'},
# 'slug': {'style': 'width: 400px'},
# }
# form_args = {
# # 'body': {'widget': TextEditor()},
# 'slug': {'widget': PrepopulatedText(master='title')}
# }
# form_subdocuments = {
# 'contents': {
# 'form_subdocuments': {
# None: {
# 'form_columns': ('content', 'caption', 'purpose',
# 'order', 'thumb'),
# 'form_ajax_refs': {
# 'content': {
# 'fields': ['title', 'long_slug', 'summary']
# }
# },
# 'form_extra_fields': {
# 'thumb': ThumbField('thumb', endpoint="media")
# }
# }
# }
# },
# }
# # form_extra_fields = {}
# form_extra_fields = {
# 'add_image': ContentImageField(
# 'Add new image',
# base_path=lazy_media_path(),
# # thumbnail_size=get_setting_value('MEDIA_IMAGE_THUMB_SIZE',
# # default=(100, 100, True)),
# endpoint="media",
# namegen=dated_path,
# permission=0o777,
# allowed_extensions="MEDIA_IMAGE_ALLOWED_EXTENSIONS",
# )
# }
# # action_disallowed_list
# # page_size = 20
# # form_ajax_refs = {
# # 'main_image': {"fields": ('title',)}
# # }
# # def get_list_columns(self):
# # column_list = super(BaseContentAdmin, self).get_list_columns()
# # if get_setting_value('SHORTENER_ENABLED'):
# # column_list += [('short_url', 'Short URL')]
# # return column_list
# def after_model_change(self, form, model, is_created):
# if not hasattr(form, 'add_image'):
# return
# form.add_image.save_contents(model)
PK KW
S quokka/admin/widgets.py
from flask import current_app
from quokka.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
PK K<X! ! quokka/admin/wtforms_html5.py# -*- 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 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)
PK <~Kri i quokka/core/__init__.py# coding: utf-8
from inspect import getargspec
import import_string
def configure_extension(name, **kwargs):
configurator = import_string(name)
args = getargspec(configurator).args
configurator(**{key: val for key, val in kwargs.items() if key in args})
def configure_extensions(app, admin=None):
"""Configure extensions provided in config file"""
extensions = app.config.get(
'CORE_EXTENSIONS', []
) + app.config.get(
'EXTRA_EXTENSIONS', []
)
for configurator_name in extensions:
configure_extension(configurator_name, app=app, admin=admin)
return app
PK KW; quokka/core/app.py# coding: utf-8
from flask import Blueprint, Flask
from flask.helpers import _endpoint_from_view_func
class QuokkaApp(Flask):
"""
Implements customizations on Flask
- custom add_quokka_url_rule
- properties to access db and admin
"""
def add_quokka_url_rule(self, rule, endpoint=None,
view_func=None, **options):
"""Builds urls using quokka. prefix to avoid conflicts
with external modules urls."""
if endpoint is None:
endpoint = _endpoint_from_view_func(view_func)
if not endpoint.startswith('quokka.'):
endpoint = 'quokka.' + endpoint
self.add_url_rule(rule, endpoint, view_func, **options)
@property
def db(self):
return self.extensions['db']
@property
def admin(self):
ext = self.extensions['admin']
return ext[0] if isinstance(ext, list) else ext
class QuokkaModule(Blueprint):
"""Overwrite blueprint namespace to quokka.modules.name
to avoid conflicts with external Blueprints use same name"""
def __init__(self, name, *args, **kwargs):
name = "quokka.modules." + name
super(QuokkaModule, self).__init__(name, *args, **kwargs)
PK }K\ \ quokka/core/auth.py# coding: utf-8
from flask import current_app
from quokka.admin.views import ModelView
from quokka.admin.forms import Form, fields
from werkzeug.security import check_password_hash, generate_password_hash
from flask_simplelogin import SimpleLogin
def create_user(**data):
if 'username' not in data or 'password' not in data:
raise ValueError('username and password are required.')
data['_id'] = data['username']
data['password'] = generate_password_hash(
data.pop('password'),
method='pbkdf2:sha256'
)
current_app.db.users.insert_one(data)
return data
class UserForm(Form):
username = fields.StringField('Username')
email = fields.StringField('Email')
password = fields.PasswordField('Password')
class UserView(ModelView):
column_list = ('username', 'email')
column_sortable_list = ('username', 'email')
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')
# todo reencrypt password if changed
return model
def validate_login(user):
db_user = current_app.db.users.find_one({"_id": user['username']})
if not db_user:
return False
if check_password_hash(db_user['password'], user['password']):
return True
return False
def configure(app):
SimpleLogin(app, login_checker=validate_login)
def configure_user_admin(app):
app.admin.register(
app.db.users,
UserView,
name='Users',
category='Administration'
)
PK K7i quokka/core/blueprints.py# coding: utf-8
import importlib
import os
from quokka.core.commands_collector import CommandsCollector
# def load_from_packages(app):
# pass
def load_from_folder(app):
"""
This code looks for any modules or packages in the given
directory, loads them
and then registers a blueprint
- blueprints must be created with the name 'module'
Implemented directory scan
Bulk of the code taken from:
https://github.com/smartboyathome/
Cheshire-Engine/blob/master/ScoringServer/utils.py
"""
blueprints_path = app.config.get('BLUEPRINTS_PATH', 'modules')
path = os.path.join(
app.config.get('PROJECT_ROOT', '..'),
blueprints_path
)
base_module_name = ".".join([app.name, blueprints_path])
dir_list = os.listdir(path)
mods = {}
object_name = app.config.get('BLUEPRINTS_OBJECT_NAME', 'module')
module_file = app.config.get('BLUEPRINTS_MODULE_NAME', 'main')
blueprint_module = module_file + '.py'
for fname in dir_list:
if not os.path.exists(os.path.join(path, fname, 'DISABLED')) and \
os.path.isdir(os.path.join(path, fname)) and \
os.path.exists(os.path.join(path, fname, blueprint_module)):
# register blueprint object
module_root = ".".join([base_module_name, fname])
module_name = ".".join([module_root, module_file])
mods[fname] = importlib.import_module(module_name)
blueprint = getattr(mods[fname], object_name)
app.logger.info("registering blueprint: %s" % blueprint.name)
app.register_blueprint(blueprint)
# register admin
try:
importlib.import_module(".".join([module_root, 'admin']))
except ImportError as e:
app.logger.info(
"%s module does not define admin or error: %s", fname, e
)
app.logger.info("%s modules loaded", mods.keys())
def get_blueprint_commands(path, root, app_name):
modules_path = os.path.join(root, path)
base_module_name = ".".join([app_name, path])
cmds = CommandsCollector(modules_path, base_module_name)
return cmds
def blueprint_commands(app=None):
return get_blueprint_commands(
path=app.config.get('BLUEPRINTS_PATH', 'modules'),
root=app.config.get('PROJECT_ROOT', '..'),
app_name=app.name
)
PK KJ8 ! quokka/core/commands_collector.pyimport importlib
import os
import sys
import click
class CommandsCollector(click.MultiCommand):
"""A MultiCommand to collect all click commands from a given
modules path and base name for the module.
The commands functions needs to be in a module inside commands
folder and the name of the file will be used as the command name.
"""
def __init__(self, modules_path, base_module_name, **attrs):
click.MultiCommand.__init__(self, **attrs)
self.base_module_name = base_module_name
self.modules_path = modules_path
def list_commands(self, *args, **kwargs):
commands = []
for _path, _dir, _ in os.walk(self.modules_path):
if 'commands' not in _dir:
continue
for filename in os.listdir(os.path.join(_path, 'commands')):
if filename.endswith('.py') and filename != '__init__.py':
cmd = filename[:-3]
_, module_name = os.path.split(_path)
commands.append('{0}_{1}'.format(module_name, cmd))
commands.sort()
return commands
def get_command(self, ctx, name):
try:
if sys.version_info[0] == 2:
name = name.encode('ascii', 'replace')
splitted = name.split('_')
if len(splitted) <= 1:
return
module_name, command_name = splitted
if not all([module_name, command_name]):
return
module = '{0}.{1}.commands.{2}'.format(
self.base_module_name,
module_name,
command_name)
mod = importlib.import_module(module)
except ImportError:
return
return getattr(mod, 'cli', None)
PK TKv] ! quokka/core/context_processors.py# coding: utf-8
# import datetime
def configure(app):
@app.context_processor
def inject():
# now = datetime.datetime.now()
return dict(
# channels=Channel.objects(published=True,
# available_at__lte=now,
# parent=None),
# Config=Config,
# Content=Content,
# Channel=Channel,
# homepage=Channel.get_homepage(),
# Link=Link
)
PK qKWa quokka/core/db.pyfrom pymongo import MongoClient
from tinydb_serialization import SerializationMiddleware
from tinymongo import TinyMongoClient
from tinymongo.serializers import DateTimeSerializer
class QuokkaTinyMongoClient(TinyMongoClient):
@property
def _storage(self):
serialization = SerializationMiddleware()
serialization.register_serializer(DateTimeSerializer(), 'TinyDate')
# TODO: Read custom serializers from settings and extensions
return serialization
class QuokkaDB:
def __init__(self, app=None):
self.app = None
if app is not None:
self.init_app(app)
def init_app(self, app):
self.config = app.config.get('DATABASE', {})
self.system = self.config.get('system', 'tinydb')
self.folder = self.config.get('folder', 'databases')
self.username = self.config.get('username')
self.password = self.config.get('password')
self.host = self.config.get('host', 'localhost')
self.port = self.config.get('port', 'port')
self.name = self.config.get('name', 'quokka_db')
self.collections = {
'index': 'index',
'contents': 'contents',
'uploads': 'uploads',
'users': 'users',
}
self.collections.update(self.config.get('collections', {}))
self._register(app)
def _register(self, app):
if not hasattr(app, 'extensions'):
app.extensions = {}
if 'db' in app.extensions:
raise RuntimeError("Flask extension already initialized")
app.extensions['db'] = self
self.app = app
def get_db_name(self, collection):
"""return db_name for collection"""
if self.system == "mongo":
return self.name
return collection
def get_collection(self, collection):
col_name = self.collections.get(collection, collection)
db_name = self.get_db_name(col_name)
return self.connection[db_name][col_name]
@property
def connection(self):
if getattr(self, '_connection', None) is None:
if self.system == 'tinydb':
self._connection = QuokkaTinyMongoClient(self.folder)
elif self.system == 'mongo':
self._connection = MongoClient(
host=self.host,
port=self.port,
username=self.username,
password=self.password
)
return self._connection
@property
def index(self):
return self.get_collection('index')
@property
def contents(self):
return self.get_collection('contents')
@property
def uploads(self):
return self.get_collection('uploads')
@property
def users(self):
return self.get_collection('users')
PK `KA~ quokka/core/error_handlers.py# coding: utf-8
from quokka.core.template import render_template
def configure(app):
if app.config.get('DEBUG') is True:
app.logger.debug('Skipping error handlers in Debug mode')
return
@app.errorhandler(403)
def forbidden_page(*args, **kwargs):
"""
The server understood the request, but is refusing to fulfill it.
Authorization will not help and the request SHOULD NOT be repeated.
If the request method was not HEAD and the server wishes to make public
why the request has not been fulfilled, it SHOULD describe the
reason for
the refusal in the entity. If the server does not wish to make this
information available to the client, the status code 404 (Not Found)
can be used instead.
"""
return render_template("errors/access_forbidden.html"), 403
@app.errorhandler(404)
def page_not_found(*args, **kwargs):
"""
The server has not found anything matching the Request-URI.
No indication
is given of whether the condition is temporary or permanent.
The 410 (Gone)
status code SHOULD be used if the server knows, through some internally
configurable mechanism, that an old resource is permanently unavailable
and has no forwarding address. This status code is commonly used when
the
server does not wish to reveal exactly why the request has been
refused,
or when no other response is applicable.
"""
return render_template("errors/page_not_found.html"), 404
@app.errorhandler(405)
def method_not_allowed_page(*args, **kwargs):
"""
The method specified in the Request-Line is not allowed for the
resource
identified by the Request-URI. The response MUST include an
Allow header
containing a list of valid methods for the requested resource.
"""
return render_template("errors/method_not_allowed.html"), 405
@app.errorhandler(500)
def server_error_page(*args, **kwargs):
return render_template("errors/server_error.html"), 500
# URLBUILD Error Handlers
def admin_icons_error_handler(error, endpoint, *args, **kwargs):
"when some of default dashboard button is deactivated, avoids error"
if endpoint in [item[0] for item in app.config.get('ADMIN_ICONS', [])]:
return '/admin'
raise error
app.url_build_error_handlers.append(admin_icons_error_handler)
PK KN$7 7 quokka/core/errors.pyfrom tinymongo.errors import DuplicateKeyError # noqa
PK "KJ. quokka/core/flask_dynaconf.py# coding: utf-8
"""This core extension cannot be loaded from settings.yml
should be loaded directly in create_app"""
from dynaconf.contrib import FlaskDynaconf
# from quokka.config import settings
def configure_dynaconf(app):
FlaskDynaconf(
app,
ENVVAR_FOR_DYNACONF="QUOKKA_SETTINGS_MODULE",
DYNACONF_NAMESPACE='QUOKKA',
SETTINGS_MODULE_FOR_DYNACONF='settings.yml',
YAML='.secrets.yml' # extra yaml file override ^
)
PK kK*.: quokka/core/logger.pyimport logging
def configure(app):
if app.config.get("LOGGER_ENABLED"):
logging.basicConfig(
level=getattr(logging, app.config.get("LOGGER_LEVEL", "DEBUG")),
format=app.config.get(
"LOGGER_FORMAT",
'%(asctime)s %(name)-12s %(levelname)-8s %(message)s'),
datefmt=app.config.get("LOGGER_DATE_FORMAT", '%d.%m %H:%M:%S')
)
PK צpJ(e quokka/core/monitoring.py# coding: utf-8
try:
from flask_debugtoolbar import DebugToolbarExtension
except ImportError:
DebugToolbarExtension = None
try:
from opbeat.contrib.flask import Opbeat
except ImportError:
Opbeat = None
try:
from raven.contrib.flask import Sentry
except ImportError:
Sentry = None
def configure(app, admin=None):
if app.config.get('DEBUG_TOOLBAR_ENABLED'):
try:
DebugToolbarExtension(app)
except TypeError:
raise ImportError('You must install flask_debugtoolbar')
if app.config.get('OPBEAT'):
try:
Opbeat(
app,
logging=app.config.get('OPBEAT', {}).get('LOGGING', False)
)
app.logger.info('opbeat configured!!!')
except TypeError:
raise ImportError('You must install opbeat')
if app.config.get('SENTRY_ENABLED', False):
try:
app.sentry = Sentry(app)
except TypeError:
raise ImportError('You must install raven (Sentry)')
PK ֎K9{ quokka/core/template.py# -*- coding: utf-8 -*-
from flask import current_app, session
from quokka_themes import render_theme_template
def render_template(template, theme=None, **context):
theme = theme or []
if not isinstance(theme, (list, tuple)):
theme = [theme]
sys_theme = session.get('theme', current_app.config.get('DEFAULT_THEME'))
if sys_theme:
theme.append(sys_theme)
return render_theme_template(theme, template, **context)
PK KLe{ quokka/core/template_filters.py# coding: utf-8
# TODO load from blueprints
import sys
from flask import Blueprint
from pymongo.mongo_client import MongoClient
from quokka.core.app import QuokkaModule
from quokka.core.models.content import Content
from quokka_themes import Theme
from werkzeug.routing import Rule
if sys.version_info.major == 3:
unicode = lambda x: u'{}'.format(x) # noqa # flake8: noqa
basestring = str # noqa # flake8: noqa
basetypes = (
int, str, float, dict, list, tuple,
Blueprint, QuokkaModule, Theme, Rule, MongoClient,
basestring, unicode
)
def is_instance(v, cls):
cls_map = {
tp.__name__: tp for tp in basetypes
}
return isinstance(v, cls_map.get(cls, str))
def get_content(**kwargs):
try:
return Content.objects.get(**kwargs)
except:
return None
def get_contents(limit=None, order_by="-created_at", **kwargs):
contents = Content.objects.filter(**kwargs).order_by(order_by)
if limit:
contents = contents[:limit]
return contents
def configure(app):
app.jinja_env.filters['isinstance'] = is_instance
app.add_template_global(get_content)
app.add_template_global(get_contents)
PK }H
~ quokka/core/themes.py# coding: utf-8
from quokka_themes import Themes
def configure(app):
themes = Themes()
themes.init_themes(app, app_identifier="quokka")
PK :KIX9 quokka/core/views.py# coding: utf-8
import os
from flask import current_app, redirect, request, send_from_directory, url_for
from flask_security import roles_accepted
from quokka.core.views import (ContentDetail, ContentDetailPreview,
ContentList, TagList)
from quokka.views import FeedAtom, FeedRss, SiteMap, TagAtom, TagRss
# from quokka.core.models.channel import Channel
@roles_accepted('admin', 'developer')
def template_files(filename):
template_path = os.path.join(current_app.root_path,
current_app.template_folder)
return send_from_directory(template_path, filename)
@roles_accepted('admin', 'developer')
def theme_template_files(identifier, filename):
template_path = os.path.join(
current_app.root_path,
"themes",
identifier,
"templates"
)
return send_from_directory(template_path, filename)
def media(filename):
return send_from_directory(current_app.config.get('MEDIA_ROOT'), filename)
def static_from_root():
return send_from_directory(current_app.static_folder, request.path[1:])
def configure(app):
@app.route('/favicon.ico')
def favicon():
return redirect(url_for('static', filename='favicon.ico'), code=301)
app.add_quokka_url_rule('/sitemap.xml',
view_func=SiteMap.as_view('sitemap'))
app.add_quokka_url_rule('/mediafiles/', view_func=media)
app.add_quokka_url_rule('/template_files/',
view_func=template_files)
app.add_quokka_url_rule(
'/theme_template_files//',
view_func=theme_template_files
)
for filepath in app.config.get('MAP_STATIC_ROOT', []):
app.add_quokka_url_rule(filepath, view_func=static_from_root)
# Match content detail, .html added to distinguish from channels
# better way? how?
content_extension = app.config.get("CONTENT_EXTENSION", "html")
app.add_quokka_url_rule('/.{0}'.format(content_extension),
view_func=ContentDetail.as_view('detail'))
# Draft preview
app.add_quokka_url_rule('/.preview',
view_func=ContentDetailPreview.as_view('preview'))
# Atom Feed
app.add_quokka_url_rule(
'/.atom',
view_func=FeedAtom.as_view('atom_list')
)
app.add_quokka_url_rule(
'/tag/.atom', view_func=TagAtom.as_view('atom_tag')
)
# RSS Feed
app.add_quokka_url_rule(
'/.xml', view_func=FeedRss.as_view('rss_list')
)
app.add_quokka_url_rule('/tag/.xml',
view_func=TagRss.as_view('rss_tag'))
# Tag list
app.add_quokka_url_rule('/tag//', view_func=TagList.as_view('tag'))
# Match channels by its long_slug mpath
app.add_quokka_url_rule('//',
view_func=ContentList.as_view('list'))
# Home page
# app.add_quokka_url_rule(
# '/',
# view_func=ContentList.as_view('home'),
# defaults={"long_slug": Channel.get_homepage('long_slug') or "home"}
# )
PK K=o! quokka/core/content/__init__.pyfrom .admin import ContentView
def configure(app):
app.admin.register(
app.db.index,
ContentView,
name='Content',
endpoint='contentview'
)
return 'content'
PK Kj!
quokka/core/content/admin.py# from flask_admin.helpers import get_form_data
import datetime as dt
from flask import current_app
from quokka.admin.forms import ValidationError
from quokka.admin.views import ModelView
from quokka.utils.text import slugify
from .formats import CreateForm, get_format
class ContentView(ModelView):
"""Base form for all contents"""
# TODO: move to base class and read from settings
details_modal = True
can_view_details = True
create_modal = True
can_export = True
export_types = ['csv', 'json', 'yaml', 'html', 'xls']
details_modal_template = 'admin/model/modals/details.html'
# create_template = 'admin/model/create.html'
edit_template = 'admin/quokka/edit.html'
# TODO: ^get edit_template from content_type
page_size = 20
can_set_page_size = True
form = CreateForm
column_list = (
'title',
'category',
'authors',
'date',
'modified',
'lang',
'status'
)
column_sortable_list = (
'title',
'category',
'authors',
'date',
'modified',
'lang',
'status'
)
# column_default_sort = 'date'
# TODO: implement scaffold_list_form in base class
# column_editable_list = ['category', 'status', 'title']
column_details_list = ['content_format']
# column_export_list = []
# column_formatters_export
# column_formatters = {fieldname: callable} - view, context, model, name
column_extra_row_actions = None
"""
List of row actions (instances of :class:`~flask_admin.model.template.
BaseListRowAction`).
Flask-Admin will generate standard per-row actions (edit, delete, etc)
and will append custom actions from this list right after them.
For example::
from flask_admin.model.template import EndpointLinkRowAction,
LinkRowAction
class MyModelView(BaseModelView):
column_extra_row_actions = [
LinkRowAction('glyphicon glyphicon-off',
'http://direct.link/?id={row_id}'),
EndpointLinkRowAction('glyphicon glyphicon-test',
'my_view.index_view')
]
"""
# form_edit_rules / form_create_rules
# form_rules = [
# # Define field set with header text and four fields
# rules.FieldSet(('title', 'category', 'tags'), 'Base'),
# # ... and it is just shortcut for:
# rules.Header('Content Type'),
# rules.Field('summary'),
# rules.Field('date'),
# # ...
# # It is possible to create custom rule blocks:
# # MyBlock('Hello World'),
# # It is possible to call macros from current context
# # rules.Macro('my_macro', foobar='baz')
# ]
# def create_form(self):
# form = super(ContentView, self).create_form()
# form.content_type.choices = [('a', 'a'), ('b', 'b')]
# return form
# @property
# def extra_js(self):
# return [
# url_for('static', filename='js/quokka_admin.js')
# ]
def edit_form(self, obj):
content_format = get_format(obj)
self.form_edit_rules = content_format.get_form_rules()
self._refresh_form_rules_cache()
return content_format.get_edit_form(obj)
def get_save_return_url(self, model, is_created):
if is_created:
return self.get_url('.edit_view', id=model['_id'])
return super(ContentView, self).get_save_return_url(model, is_created)
def on_model_change(self, form, model, is_created):
# check if exists
existent = current_app.db.index.find_one(
{'title': model['title'], 'category': model['category']}
)
duplicate_error_message = u'{0} "{1}/{2}" {3}'.format(
'duplicate error:',
model['category'],
model['title'],
'already exists.'
)
if (is_created and existent) or (
existent and existent['_id'] != model['_id']):
raise ValidationError(duplicate_error_message)
if is_created:
model['date'] = dt.datetime.now()
model['slug'] = slugify(model['title'])
else:
model['modified'] = dt.datetime.now()
get_format(model).before_save(form, model, is_created)
def after_model_change(self, form, model, is_created):
# TODO: Spawn async process for this.
# update tags, categories and authors
get_format(model).after_save(form, model, is_created)
PK wKT_ _ quokka/core/content/formats.pyimport datetime as dt
from flask import current_app
from flask_admin.helpers import get_form_data
from quokka.admin.forms import READ_ONLY, Form, fields, rules, validators
from werkzeug.utils import import_string
# Utils
def get_content_formats(instances=False):
content_formats = current_app.config.get(
'CONTENT_FORMATS',
{
'markdown': {
'choice_text': 'Markdown',
'help_text': 'Markdown text editor',
'content_format_class':
'quokka.core.content.formats.MarkdownFormat' # noqa
}
}
)
if instances:
for identifier, 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):
if field.data is not None:
items = field.data.split(',')
if len(items) > 1:
return 'You can select only one category'
# classes
class BaseForm(Form):
title = fields.StringField('Title', [validators.required()])
# todo: validade existing category/title
summary = fields.TextAreaField('Summary')
category = fields.Select2TagsField(
'Category',
[validators.CallableValidator(validate_category)],
save_as_list=False,
render_kw={'data-tags': '["hello", "world"]'},
# todo: ^ settings.default_categories + db_query
default='general'
# todo: default should come from settings
)
authors = fields.Select2TagsField(
'Authors',
[validators.required()],
save_as_list=True,
render_kw={'data-tags': '["Bruno Rocha", "Karla Magueta"]'},
# todo: settings.default_authors + current + db_query
default=['Bruno Rocha']
# todo: default should be current user if auth else O.S user else ?
)
class CreateForm(BaseForm):
"""Default create form where content format is chosen"""
content_type = fields.SelectField(
'Type',
[validators.required()],
choices=[('article', 'Article'), ('page', 'Page')]
)
content_format = fields.SmartSelect2Field(
'Format',
[validators.required()],
choices=get_content_format_choices
)
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)
# todo: ^ provide settings.default_tags + db_query
date = fields.DateTimeField(
'Date',
[validators.required()],
default=dt.datetime.now
)
# todo: ^default should be now
modified = fields.HiddenField('Modified')
# todo: ^populate on save
slug = fields.StringField('Slug')
# todo: create based on category / title
language = fields.SmartSelect2Field(
'Language',
choices=lambda: [
(lng, lng)
for lng in current_app.config.get('BABEL_LANGUAGES', ['en'])
]
)
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'
}
)
# todo: ^ published | draft
class BaseFormat(object):
identifier = None
edit_form = BaseEditForm
form_rules = None
def get_edit_form(self, obj):
return self.edit_form(get_form_data(), **obj)
def get_identifier(self):
return self.identifier or self.__class__.__name__
def get_form_rules(self):
if self.form_rules is not None:
self.form_rules.append(
rules.Field(
'csrf_token',
render_field='quokka_macros.render_hidden_field'
)
)
return self.form_rules
def before_save(self, form, model, is_created):
"""optional"""
def after_save(self, form, model, is_created):
"""optional"""
def extra_js(self):
return []
# 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
form_rules = [
rules.FieldSet(('title', 'summary')),
rules.Field('content'),
rules.FieldSet(('category', 'authors', 'tags')),
rules.FieldSet(('date', 'language')),
rules.FieldSet(('slug', 'content_type', 'content_format')),
rules.Field('published')
]
def before_save(self, form, model, is_created):
print('before save')
def after_save(self, form, model, is_created):
print('after save')
def extra_js(self):
return []
PK KV! ! quokka/module_template/README.md# This is Quokka Module Template
PK K]C C quokka/module_template/setup.pyfrom setuptools import setup
setup(name='quokka_module_template')
PK rKz&> > 9 quokka/module_template/quokka_module_template/__init__.pyfrom .admin import UserView, TweetView
def configure(app):
app.admin.register(
app.db.users,
UserView,
# category='User',
name='User'
)
app.admin.register(
app.db.get_collection('tweets'),
TweetView,
# category='User',
name='Tweets'
)
PK \KK
6 quokka/module_template/quokka_module_template/admin.pyfrom 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)
PK K ? quokka/module_template/quokka_module_template.egg-info/PKG-INFOMetadata-Version: 1.0
Name: quokka-module-template
Version: 0.0.0
Summary: UNKNOWN
Home-page: UNKNOWN
Author: UNKNOWN
Author-email: UNKNOWN
License: UNKNOWN
Description: UNKNOWN
Platform: UNKNOWN
PK Ko B quokka/module_template/quokka_module_template.egg-info/SOURCES.txtsetup.py
quokka_module_template.egg-info/PKG-INFO
quokka_module_template.egg-info/SOURCES.txt
quokka_module_template.egg-info/dependency_links.txt
quokka_module_template.egg-info/top_level.txtPK K2 K quokka/module_template/quokka_module_template.egg-info/dependency_links.txt
PK K2 D quokka/module_template/quokka_module_template.egg-info/top_level.txt
PK oKSߖ " quokka/project_template/.gitignore.secrets.yml
databases/*
!databases/README.md
uploads/*
!uploads/README.md
static_build/*
!static_build/README.md
venv
*.log
*.pid
*.cache*
*.py[cod]
PK K $ quokka/project_template/.secrets.yml# Use this file to store sensitive settings
# put it in your .gitignore and do not push to the repo
# or even better: ALWAYS USE ENVIRONMENT VARIABLES for sensitive data
# export QUOKKA_SECRET_KEY=1234
# and dynaconf will take it from there
quokka:
SECRET_KEY: abcderf
PK SnK.ƒ " quokka/project_template/manage.ymlclick_commands: []
function_commands: []
inline_commands: []
help_text: This is the {project_name} interactive shell
project_name: Quokka Website
shell:
auto_import:
display: true
objects:
quokka.create_app:
as: app
banner:
enabled: true
message: Quokka Interactive Shell!
init_script: |
print("Starting Quokka shell!")
app = app()
readline_enabled: false
PK K8ߠ $ quokka/project_template/settings.yml# important: never change version manually
# it is used by `quokka upgrade` command
VERSION: 1.0.0
# App reads values provided under `QUOKKA` namespace.
# settings can be lower or upper case does not matter
# as internally Dynaconf transform to upper case.
QUOKKA:
# Enable login for admin access
ADMIN_REQUIRES_LOGIN: false
# Enable login for website
WEBSITE_REQUIRES_LOGIN: false
# DEBUG Mode
DEBUG: true
DEBUG_TOOLBAR_ENABLED: true
ADMIN_RAISE_ON_VIEW_EXCEPTION: true
LOGGER_ENABLED: true
# Secret Key must be in .secrets.yml or QUOKKA_SECRET_KEY env var.
# SECRET_KEY: abcderf
WTF_CSRF_ENABLED: true
# be careful touching this list, use EXTRA_EXTENSIONS for custom extensions
CORE_EXTENSIONS:
# Essential
- quokka.core.logger.configure
- quokka.core.db.QuokkaDB
- quokka.admin.configure_admin
- flask_wtf.csrf.CSRFProtect
- flask_mistune.Mistune
- quokka.core.auth.configure
- quokka.core.error_handlers.configure
- quokka.core.monitoring.configure
- quokka.core.themes.configure
- quokka.core.content.configure
- quokka.admin.configure_file_admin
- quokka.admin.configure_extra_views
- quokka.core.auth.configure_user_admin
# Extensions here can be import path to any callable or class
# that callable will receive app as only argument.
# example:
# def configure_my_extension(app):
# app.db # <--- db pointer
# app.admin # <--- admin pointer
# load_extension_here
# publish your extension to pypi and install
# either with `pip install quokka_extension`
# or `python setup.py install`
# or (recommended) use flit.ini
# take a look at `quokka create_extension --help` command
# add to the list below `- quokka_extension.configure`
EXTRA_EXTENSIONS:
- quokka_module_template.configure
CONTENT_FORMATS:
markdown:
choice_text: Markdown
help_text: Markdown text editor
content_format_class: quokka.core.content.formats.MarkdownFormat
html:
choice_text: HTML
help_text: Rich HTML text editor
content_format_class: quokka.core.content.formats.HTMLFormat
plaintext:
choice_text: Plain Text
help_text: Pure plain text in text area
content_format_class: quokka.core.content.formats.PlainFormat
# if true any package named quokka_ will be loaded
# http://flask.pocoo.org/docs/0.12/cli/#cli-plugins
AUTO_LOAD_EXTENSIONS: true
# else provide a list
# Plugin settings might be namespaced in dynaconf
DATABASE:
# TinyDB
system: tinydb
folder: databases
# Mongo
# system: mongo
# username: admin
# password: secret
# host: myhost.com
# port: 27017
# Database name
name: quokka_db
collections:
index: index
contents: contents
uploads: uploads
users: users
DEFAULT_TEXT_FORMAT: markdown
ADMIN:
name: Quokka NG Admin
url: /admin
# https://bootswatch.com/
FLASK_ADMIN_SWATCH: sandstone
FLASK_ADMIN_TEMPLATE_MODE: bootstrap3
# ADMIN_ICONS: [
# ['post.create_view', 'pencil', 'Write'],
# ['post.index_view', 'th-list', 'Posts'],
# ['config.index_view', 'cog', 'Config'],
# ['user.index_view', 'user', 'Users'],
# ['image.index_view', 'picture', 'Images'],
# ['image.create_view', 'arrow-up', 'Upload'],
# ['channel.index_view', 'th-list', 'Channels']
# ]
DEFAULT_EDITABLE_EXTENSIONS: &extensions [
'html', 'css', 'js', 'py', 'txt', 'md', 'cfg', 'coffee', 'html', 'json',
'xml', 'yaml', 'yml', 'HTML', 'CSS', 'JS', 'PY', 'TXT', 'MD', 'CFG',
'COFFEE', 'HTML', 'JSON', 'XML', 'YAML', 'YML'
]
FILE_ADMIN:
- name: Themes
category: Files
path: themes
url: /themesfiles/ # create nginx rule
endpoint: themes_files
editable_extensions: *extensions
- name: Uploads
category: Files
path: uploads
url: /uploadsfiles/ # Create nginx rule
endpoint: uploads_files
editable_extensions: *extensions
- name: Databases
category: Files
path: databases
url: /databasesfiles/ # Create nginx rule
endpoint: databases_files
editable_extensions: *extensions
# All this languages will be available to translate admin panel
# and also as language to post content
BABEL_LANGUAGES: &languages [
'en', 'cs', 'de', 'es', 'fa', 'fr', 'pt', 'ru', 'pa', 'zh_CN', 'zh_TW'
]
# this will be the default language on admin and front end
# admin language can also be user based
BABEL_DEFAULT_LOCALE: &default_locale pt
# Home page and categories lists all content on this languages no filter
# should be a list e.g: ['en'] to filter only english by default
# anyway accessing site.com/LANG/ would filter specific language
# static site generation loops on *languages
# translations variable list all translations urls in theme
FILTER_LANGUAGES: *languages
LOGGER_LEVEL: DEBUG
LOGGER_FORMAT: '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
LOGGER_DATE_FORMAT: '%d.%m %H:%M:%S'
DEBUG_TB_INTERCEPT_REDIRECTS: false
DEBUG_TB_PROFILER_ENABLED: true
DEBUG_TB_TEMPLATE_EDITOR_ENABLED: true
DEBUG_TB_PANELS:
- flask_debugtoolbar.panels.versions.VersionDebugPanel
- flask_debugtoolbar.panels.timer.TimerDebugPanel
- flask_debugtoolbar.panels.headers.HeaderDebugPanel
- flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel
- flask_debugtoolbar.panels.template.TemplateDebugPanel
# TODO: Migrate mongoengine panel to tinyDB and Pymongo
# - flask_mongoengine.panels.MongoDebugPanel
- flask_debugtoolbar.panels.logger.LoggingPanel
- flask_debugtoolbar.panels.profiler.ProfilerDebugPanel
- flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel
DEFAULT_THEME: Flex
# https://github.com/alexandrevicenzi/Flex
# https://github.com/alexandrevicenzi/Flex/wiki/Custom-Settings
# http://www.pelicanthemes.com/
# $ quokka themes install theme-name
# $ quokka themes activate theme-name
# http://docs.getpelican.com/en/latest/themes.html
THEME_PATHS: themes
# content urls will end with .html
CONTENT_EXTENSION: html
# monitoring
# You must install `raven` package to use it
SENTRY_ENABLED: false
# Sentry DSN Key must be in .secrets.yml or QUOKKA_SENTRY_DSN env var.
# SENTRY_DSN: ''
# https://opbeat.com is application monitoring tool
# you can enable it but you need to install opbeat
# https://opbeat.com/docs/articles/get-started-with-flask/
# OPBEAT must be in .secrets.yml or QUOKKA_OPBEAT='@json {..}' env var.
# you must install `opbeat` package to use it
# OPBEAT:
# ORGANIZATION_ID: ''
# APP_ID: ''
# SECRET_TOKEN: ''
# INCLUDE_PATHS: ['quokka']
# DEBUG: true
# LOGGING': False
PK KW4 4 quokka/project_template/wsgi.py#!/usr/bin/python
import argparse
from werkzeug.serving import run_simple
from quokka import create_app
application = app = create_app()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run Quokka App for WSGI")
parser.add_argument('-p', '--port', help='App Port')
parser.add_argument('-i', '--host', help='App Host')
parser.add_argument('-r', '--reloader', action='store_true',
help='Turn reloader on')
parser.add_argument('-d', '--debug', action='store_true',
help='Turn debug on')
args = parser.parse_args()
run_simple(
args.host or '0.0.0.0',
int(args.port) if args.port else 5000,
application,
use_reloader=args.reloader or False,
use_debugger=args.debug or False,
)
PK oJ + quokka/project_template/databases/README.mdPK nKZ4 / quokka/project_template/databases/__name__.json{"_default": {}}PK nKZ4 3 quokka/project_template/databases/__objclass__.json{"_default": {}}PK nKZ4 O quokka/project_template/databases/_ipython_canary_method_should_not_exist_.json{"_default": {}}PK nK7{ / quokka/project_template/databases/contents.json{"_default": {}, "contents": {}}PK K-*9 , quokka/project_template/databases/index.json{"_default": {}, "index": {"16": {"title": "hello", "summary": "", "category": "general", "authors": ["Bruno Rocha"], "content_type": "article", "content_format": "markdown", "csrf_token": "IjVlYzQ4YTI5YmYwZTQ4M2ZlZGU2MzhkOTdkYzdiOGU1MzJjZTkzNTIi.C85_bw.5M8czZl-kBiUFkRpAaow57D4bjs", "date": "{TinyDate}:2017-04-05T05:43:11", "slug": "hello", "_id": "e69143e819db11e7b53f5ce0c5482b4b", "tags": [], "language": "en", "published": false, "content": "", "modified": "{TinyDate}:2017-04-11T11:53:39"}, "17": {"title": "bla", "summary": "", "category": "general", "authors": ["Bruno Rocha"], "content_type": "article", "content_format": "markdown", "csrf_token": "IjQwMDgwYjRlMzAwYTYyMzg0YzU4NDhkNzlkMzVmNTRmMDYwNDYzYTMi.C8Y_vg.a8yN6Z2hvr_sFXJ7zMMcr5Rwxec", "date": "{TinyDate}:2017-04-05T05:43:42", "slug": "bla", "_id": "f8f3f40e19db11e7b53f5ce0c5482b4b", "tags": [], "language": "en", "published": false, "content": ""}, "18": {"title": "hello2", "summary": "haaslkdnas", "category": "general", "authors": ["Bruno Rocha"], "content_type": "article", "content_format": "markdown", "csrf_token": "IjVlYzQ4YTI5YmYwZTQ4M2ZlZGU2MzhkOTdkYzdiOGU1MzJjZTkzNTIi.C8546w.WJECsaiaJAJDDLRHz2GRlH3nYNg", "date": "{TinyDate}:2017-04-11T11:25:51", "slug": "hello2", "_id": "c37e96d01ec211e7973752540036efda"}, "19": {"title": "dfgdfgdfgdf", "summary": "gdfgdfgdfgdfgdfgdfg", "category": "general", "authors": ["Bruno Rocha"], "content_type": "article", "content_format": "markdown", "csrf_token": "IjQzZmZlODBlODcyYzgyMTBkNjcwZDQ2MjdhN2MyNzdkMTBkZGVlMWUi.DIO1Cg.pPkve337AgJP1khWeCAi1JZN5n4", "date": "{TinyDate}:2017-08-26T22:43:03", "slug": "dfgdfgdfgdf", "_id": "1119181c8ac911e7b5d55ce0c5482b4b"}, "20": {"title": "bla bla", "summary": "sdfsdfsdfsdfsdfsdf", "category": "general", "authors": ["Bruno Rocha"], "content_type": "article", "content_format": "markdown", "csrf_token": "IjQzZmZlODBlODcyYzgyMTBkNjcwZDQ2MjdhN2MyNzdkMTBkZGVlMWUi.DIO1Nw.HeA7BcSE6yZ3f2wKPtyJr_K4mhE", "date": "{TinyDate}:2017-08-26T22:43:26", "slug": "bla-bla", "_id": "1eeb893e8ac911e7b5d55ce0c5482b4b"}, "21": {"title": "sfdsf", "summary": "sdfsdfsdf", "category": "world", "authors": ["Bruno Rocha"], "content_type": "article", "content_format": "markdown", "csrf_token": "IjQzZmZlODBlODcyYzgyMTBkNjcwZDQ2MjdhN2MyNzdkMTBkZGVlMWUi.DIO5iw.LxBs7zMaW4JjbSBI4vP5496d74E", "date": "{TinyDate}:2017-08-26T23:01:53", "slug": "sfdsf", "_id": "b25aa04a8acb11e7b5d55ce0c5482b4b"}, "22": {"title": "sfsdf", "summary": "sdfsdfsdf", "category": "banana", "authors": ["Jon Silva"], "content_type": "article", "content_format": "markdown", "csrf_token": "IjQzZmZlODBlODcyYzgyMTBkNjcwZDQ2MjdhN2MyNzdkMTBkZGVlMWUi.DIO5sQ.ywUjzf8m0O6G7rpo9IwWKNhbC0w", "date": "{TinyDate}:2017-08-26T23:02:30", "slug": "sfsdf", "_id": "c83af61c8acb11e7b5d55ce0c5482b4b"}, "23": {"title": "hello3", "summary": "vlasdasdasdasdasd", "category": "banana", "authors": ["Bruno Rocha"], "content_type": "article", "content_format": "markdown", "csrf_token": "IjQzZmZlODBlODcyYzgyMTBkNjcwZDQ2MjdhN2MyNzdkMTBkZGVlMWUi.DIYP5g.TBra3SKtoUzhAMobEqb7K5NNTCU", "date": "{TinyDate}:2017-08-28T17:34:14", "slug": "hello3", "_id": "4196fa1e8c3011e7a91a5ce0c5482b4b", "tags": [], "language": "en", "published": true, "content": "", "modified": "{TinyDate}:2017-08-28T17:34:53"}}}PK `{KZ4 + quokka/project_template/databases/meta.json{"_default": {}}PK *K! 0 quokka/project_template/databases/quokka_db.json{"_default": {}, "index": {}}PK K0 - quokka/project_template/databases/tweets.json{"_default": {}, "tweets": {}}PK nK . quokka/project_template/databases/uploads.json{"_default": {}, "uploads": {}}PK Kn n , quokka/project_template/databases/users.json{"_default": {}, "users": {"1": {"_id": "admin2", "username": "admin2", "password": "", "email": "admin@admin.com", "csrf_token": "IjQxYWYyYzMzZWUzMmEyNzA4Mjk5MzJkYjI0YTJhNWFkMjQzNDU0YzIi.C7Dl1w.wnkHUNuHC6edesxQ7Ybs92D0QEQ"}, "2": {"_id": "admin3", "username": "admin3", "password": "pbkdf2:sha256:50000$uI8OyY3a$5874945cbfe8dd216e4b84062799afbbc4c07879868c4386a030e62428508576", "email": "admin@admin.com"}, "3": {"username": "admin4", "email": "admin@admin.com", "_id": "admin4", "password": "pbkdf2:sha256:50000$q9BH811i$17487be4985a794b369f10429ef79e568d25b7d16589ede83ea583f9c70dd2c5"}, "4": {"username": "admin5", "email": "admin@admin.com", "_id": "admin5", "password": "pbkdf2:sha256:50000$ja0doNlk$10ce6429ddbb1b542c4b8f84b5a4b6865b1e4f88b6f68e5f7946017e8027cb14"}, "5": {"username": "admin6", "email": "admin@admin.com", "_id": "admin6", "password": "pbkdf2:sha256:50000$vl2vdYOi$8c52ec88630a34e2fdcfb6f6bf4b102d574655d0f3b3191919f8bbc9fb4943a0"}, "6": {"username": "bobo", "email": "bobo@blo.com", "_id": "bobo", "password": "pbkdf2:sha256:50000$CQ14cePV$601ea98a2f07bc3ea36f6a5cfe06396f576339564307194c1b32b7708f7c4019"}}}PK OoJ . quokka/project_template/static_build/README.mdPK oJ ( quokka/project_template/themes/README.mdPK (
nJz . quokka/project_template/themes/Flex/.gitignore# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
tests/output
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
venv
# NPM
node_modules
PK (
nJa / quokka/project_template/themes/Flex/.travis.ymllanguage: python
python:
- "2.7"
- "3.4"
- "3.5"
install:
- pip install pelican markdown
before_script:
- git clone https://github.com/getpelican/pelican-plugins plugins
script: pelican -s tests/pelicanconf.py
notifications:
email: false
PK (
nJnb b 0 quokka/project_template/themes/Flex/CHANGELOG.md## 2.1.0
- Italian translation [#60](https://github.com/alexandrevicenzi/Flex/issues/60)
- Hungarian transltion [#59](https://github.com/alexandrevicenzi/Flex/issues/59)
- Russian transltion [#58](https://github.com/alexandrevicenzi/Flex/issues/58)
- [Google AdSense support](https://github.com/alexandrevicenzi/Flex/issues/47)
## 2.0.0
- [Minute read like Medium](https://github.com/alexandrevicenzi/Flex/issues/48) (via plugin)
- [Theme translations](https://github.com/alexandrevicenzi/Flex/wiki/Translation-support)
- Updated font-awesome
- Changed `Cover` metadata to use relative path.
This version includes de, fr and pt_BR translations.
Special thanks to @marcelhuth.
## 1.2.0
- [Update font-awesome](https://github.com/alexandrevicenzi/Flex/issues/31)
- [Added browser color configuration](https://github.com/alexandrevicenzi/Flex/pull/34)
- [Related posts](https://github.com/alexandrevicenzi/Flex/pull/27)
- [More Pygments Styles](https://github.com/alexandrevicenzi/Flex/issues/38)
- [Add StatusCake RUM support](https://github.com/alexandrevicenzi/Flex/issues/16)
## 1.1.1
- [Bug in CSS with placement of "Newer Posts" button](https://github.com/alexandrevicenzi/Flex/issues/21)
- [Posts preview on main page](https://github.com/alexandrevicenzi/Flex/issues/14)
- [Strip HTML tags from title](https://github.com/alexandrevicenzi/Flex/pull/25)
- [Added style for reddit social link](https://github.com/alexandrevicenzi/Flex/pull/23)
## 1.1.0
- [Allow custom CSS stylesheets to override the default one](https://github.com/alexandrevicenzi/Flex/pull/9)
- [Add Windows-specific font variants](https://github.com/alexandrevicenzi/Flex/pull/8)
- [Move the "tagged" bullet inside the conditional.](https://github.com/alexandrevicenzi/Flex/pull/7)
- [Add stack-overflow to supported social icons](https://github.com/alexandrevicenzi/Flex/pull/6)
- [Use THEME_STATIC_DIR for asset URL's](https://github.com/alexandrevicenzi/Flex/pull/5)
- [show summary for articles in index.html](https://github.com/alexandrevicenzi/Flex/pull/4)
- [Fixed email icon bug](https://github.com/alexandrevicenzi/Flex/pull/3)
## 1.0.0
First release.
PK (
nJ1g= = + quokka/project_template/themes/Flex/LICENSEThe MIT License (MIT)
Copyright (c) 2015 Alexandre Vicenzi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
PK (
nJc?, , - quokka/project_template/themes/Flex/README.md# Flex [](https://travis-ci.org/alexandrevicenzi/Flex) [](https://david-dm.org/alexandrevicenzi/Flex) [](https://gitter.im/alexandre-vicenzi/flex?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
The minimalist [Pelican](http://blog.getpelican.com/) theme.
## Note
DON'T send any questions, issues or anything related to Flex to my personal email. They will be IGNORED by now. Your question is someone else question. They SHOULD be public, so others can know how to fix configuration problems.
## Features
- Mobile First
- Responsive
- Semantic
- SEO Best Practices
- Open Graph
- Rich Snippets (JSON-LD)
- Related Posts (via [plugin](https://github.com/getpelican/pelican-plugins/tree/master/related_posts) or AddThis)
- Minute read (via [plugin](https://github.com/getpelican/pelican-plugins/tree/master/post_stats)) (new in 2.0)
- [Multiple Code Highlight Styles](https://github.com/alexandrevicenzi/Flex/wiki/Code-Highlight)
- [Translation Support](https://github.com/alexandrevicenzi/Flex/wiki/Translations) (new in 2.0)
## Integrations
- [AddThis](http://www.addthis.com/) Share Buttons and Related Posts
- [Disqus](https://disqus.com/)
- [Gauges](http://get.gaug.es/)
- [Google AdSense](https://www.google.com.br/adsense/start/) (new in 2.1)
- [Google Analytics](https://www.google.com/analytics/web/)
- [Google Tag Manager](https://www.google.com/tagmanager/)
- [Piwik](http://piwik.org/)
- [StatusCake](https://www.statuscake.com/)
- [Github Corners](https://github.com/tholman/github-corners) (new in 2.2)
## Plugins Support
- [I18N Sub-sites](https://github.com/getpelican/pelican-plugins/tree/master/i18n_subsites) (new in 2.0)
- [Related Posts](https://github.com/getpelican/pelican-plugins/tree/master/related_posts)
- [Representative image](https://github.com/getpelican/pelican-plugins/tree/master/representative_image) (new in 2.2)
## Install
The recommend way to install is over [pelican-themes](https://github.com/getpelican/pelican-themes).
The `master` branch is the development branch. If you're happy with fresh new things and maybe sometimes (~most of time~) broken things you can clone the `master`, but I would recommend to you to clone a tag branch.
## Documentation
[Go to Wiki](https://github.com/alexandrevicenzi/Flex/wiki)
## Contributing
Always open an issue before sending a PR. Talk about the problem/feature that you want to fix. If it's really a good thing you can submit your PR. If you send an PR without talking about before what it is, you may work for nothing.
As always, if you want something that only make sense to you, just fork Flex and start a new theme.
## Donate
Did you liked this theme? Buy me a beer and support new features.
### PayPal
[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=KZCMH3N74KKFN&lc=BR&item_name=Alexandre%20Vicenzi&item_number=flex¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted)
### Bitcoin

`1Heza38sS36iTtaFpWB5wziuiK9WChmwss`
## Translate
Translate this theme to new languages at [Transifex](https://www.transifex.com/alexandrevicenzi/flex-pelican/).

Read more about [Translation Support](https://github.com/alexandrevicenzi/Flex/wiki/Translations).
## Live example
You can see how things looks like [here](https://blog.alexandrevicenzi.com/flex-pelican-theme.html).
Or you can take a look at [Flex users](https://github.com/alexandrevicenzi/Flex/wiki/Flex-users).
I'm using Flex in my [personal blog](http://blog.alexandrevicenzi.com/).

## License
MIT
## Contributors
[Contributors list](https://github.com/alexandrevicenzi/Flex/graphs/contributors)
PK (
nJj; + quokka/project_template/themes/Flex/btc.pngPNG
IHDR y sBIT|d tEXtSoftware gnome-screenshot> IDATxۮ0Pry={)k!4i5L,_^~>pk}xݕj߯S.b"`qF Ep#\1
]fuWF}Er#\1ETC)&ƿEp#\1ow5SnVFOr#\1ETCܵ`u+M~M"F.bۆi_wM"F.bӧx<BV^M"F.b>+ɧXt/T.b"F^kgǾUwÀEp#\1+Ku4Si:ӿ{Ep#\1S#N:90?E*1Ep#\wL_ұ`,"F.bKUO-X1ͥZG>\1Ep>f{}OMEp#\1'vlQXWMKsG"F.b]nx"'SFY6Ep#\ļݮi$SWf>Ep#\1e