From 5b74f08fcba6e31139b43533731b6d5c10e3d72d Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Tue, 1 Dec 2015 19:48:09 -0200 Subject: [PATCH 01/65] Added aliases_map functionality. related to #17 --- quokka/core/app.py | 11 ++++- quokka/settings.py | 20 +++++++++ quokka/utils/aliases.py | 95 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 quokka/utils/aliases.py diff --git a/quokka/core/app.py b/quokka/core/app.py index 5a632f373..2fb9b8477 100644 --- a/quokka/core/app.py +++ b/quokka/core/app.py @@ -1,9 +1,14 @@ from flask import Flask, Blueprint from quokka.core.config import QuokkaConfig +from quokka.utils.aliases import dispatch_aliases class QuokkaApp(Flask): - """Implementes a customized config handler""" + """ + Implementes customizations on Flask + - Config handler + - Aliases dispatching before request + """ config_class = QuokkaConfig @@ -14,6 +19,10 @@ def make_config(self, instance_relative=False): root_path = self.instance_path return self.config_class(root_path, self.default_config) + def preprocess_request(self): + return dispatch_aliases() or super(QuokkaApp, + self).preprocess_request() + class QuokkaModule(Blueprint): "for future overriding" diff --git a/quokka/settings.py b/quokka/settings.py index e19767f29..5ff89199c 100644 --- a/quokka/settings.py +++ b/quokka/settings.py @@ -284,6 +284,26 @@ "Note: if you enable shortener you have to define a SERVER_NAME" # SERVER_NAME = 'localhost' +"Redirect aliases is enabled?" +ALIASES_ENABLED = True + +""" +ALIASES_MAP +keys are long_slug + keys should always start with / + & end with / or extension. +{ + "/team/": { + "alias_type": "endpoint|long_slug|url|string", + "to": "authors|/articles/science.html|http://t.co|'Hello'", + "published": True, + "available_at": "", + "available_until: "", + } +} +""" +ALIASES_MAP = {} + "Config shorter information" SHORTENER_SETTINGS = {"name": "BitlyShortener", "bitly_api_key": "R_7d84f09c68be4c749cac2a56ace2e73f", diff --git a/quokka/utils/aliases.py b/quokka/utils/aliases.py new file mode 100644 index 000000000..810d8b4c5 --- /dev/null +++ b/quokka/utils/aliases.py @@ -0,0 +1,95 @@ +from flask import ( + redirect, url_for, request, current_app, render_template_string, abort +) +from flask.globals import _app_ctx_stack, _request_ctx_stack +from werkzeug.urls import url_parse +from quokka.core.templates import render_template + + +def dispatch_aliases(): + """ + When ALIASES_ENABLED == True + + This method handle 3 QuokkaCMS features: + 1. Fixed aliases + Alias is defined in ALIASES_MAP setting as a dictionary + 2. Managed Redirects + Alias defined in database + 3. Channel and Content aliases + Alias defined in specific channel or content + + ALIASES_MAP + keys are long_slug + keys should always start with / + & end with / or extension. + { + "/team/": { + "alias_type": "endpoint|long_slug|url|string|template", + "action": "redirect|render", + "to": "authors|/articles/science.html|http://t.co|'Hello'", + "published": True, + "available_at": "", + "available_until: "", + } + } + + - 'endpoint' and 'long_slug' by default are rendered + - 'url' is always redirect + - 'string' and 'template' are always rendered + """ + + app = current_app + aliases_map = app.config.get('ALIASES_MAP') + if aliases_map and request.path in aliases_map: + alias = aliases_map[request.path] + status = alias.get('status', 200) + if alias['alias_type'] == 'endpoint': + endpoint = alias['to'] + if alias.get('action') == 'redirect': + return redirect(url_for(endpoint, **request.args)) + else: # render + return app.process_response( + app.make_response( + app.view_functions[endpoint]() + ) + ) + elif alias['alias_type'] == 'long_slug': + long_slug = alias['to'] + if alias.get('action') == 'redirect': + return redirect(long_slug) # pass request.args ? + else: # render + endpoint = route_from(long_slug)[0] + return app.process_response( + app.make_response( + app.view_functions[endpoint]() + ) + ) + elif alias['alias_type'] == 'url': + return redirect(alias['to']) + elif alias['alias_type'] == 'string': + return render_template_string(alias['to']), status + elif alias['alias_type'] == 'template': + return render_template(alias['to']), status + + +def route_from(url, method=None): + appctx = _app_ctx_stack.top + reqctx = _request_ctx_stack.top + if appctx is None: + raise RuntimeError('Attempted to match a URL without the ' + 'application context being pushed. This has to be ' + 'executed when application context is available.') + + if reqctx is not None: + adapter = reqctx.url_adapter + else: + adapter = appctx.url_adapter + if adapter is None: + raise RuntimeError('Application was not able to create a URL ' + 'adapter for request independent URL matching. ' + 'You might be able to fix this by setting ' + 'the SERVER_NAME config variable.') + parsed = url_parse(url) + if parsed.netloc is not "" and parsed.netloc != adapter.server_name: + abort(404) + return adapter.match(parsed.path, method) From 9a522d4c1db89e5386c2f965e09e103296240d3e Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 3 Dec 2015 16:10:21 -0200 Subject: [PATCH 02/65] Added "plaintext" as CONTENT_FORMAT option. --- Makefile | 2 +- quokka/core/__init__.py | 2 +- .../admin/static/admin/css/bootstrap2/quokka_admin.css | 5 +++++ quokka/themes/admin/templates/admin/base.html | 9 ++++++--- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 3bb19f575..3d4940fea 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ shell: .PHONY: test test: pep8 - QUOKKA_MODE=test py.test --cov quokka + QUOKKA_MODE=test py.test --cov=quokka quokka/ .PHONY: install install: diff --git a/quokka/core/__init__.py b/quokka/core/__init__.py index f794655c2..d3e4077ce 100644 --- a/quokka/core/__init__.py +++ b/quokka/core/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -TEXT_FORMATS = ("html", "markdown") +TEXT_FORMATS = ("html", "markdown", "plaintext") diff --git a/quokka/themes/admin/static/admin/css/bootstrap2/quokka_admin.css b/quokka/themes/admin/static/admin/css/bootstrap2/quokka_admin.css index 881c0516d..e8d4d19d6 100644 --- a/quokka/themes/admin/static/admin/css/bootstrap2/quokka_admin.css +++ b/quokka/themes/admin/static/admin/css/bootstrap2/quokka_admin.css @@ -47,3 +47,8 @@ */ #no-more-tables td:before { content: attr(data-title); } } + + +.controls .md-editor>textarea { + background: none; +} diff --git a/quokka/themes/admin/templates/admin/base.html b/quokka/themes/admin/templates/admin/base.html index a1bcae285..285bfa178 100644 --- a/quokka/themes/admin/templates/admin/base.html +++ b/quokka/themes/admin/templates/admin/base.html @@ -116,13 +116,13 @@ if (selected_format == null) { if (default_editor == 'markdown') { load_markdown_editor(); - } else { + } else if (default_editor == 'html') { load_html_editor(); } } else { if (selected_format.value == 'markdown') { load_markdown_editor(); - } else { + } else if (selected_format.value == 'html') { load_html_editor(); } @@ -130,9 +130,12 @@ if (selected_format.value == 'markdown') { destroy_html_editor(); load_markdown_editor(); - } else { + } else if (selected_format.value == 'html') { destroy_markdown_editor(); load_html_editor(); + } else { + destroy_markdown_editor(); + destroy_html_editor() } }, false); } From d1463e245bc6a3a33e21962ced35a8c801eb14ac Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 3 Dec 2015 18:50:00 -0200 Subject: [PATCH 03/65] User Profile Plugin --- quokka/core/__init__.py | 6 ++- quokka/core/fields.py | 4 ++ quokka/modules/accounts/__init__.py | 8 ++- quokka/modules/accounts/models.py | 8 +++ quokka/modules/accounts/views.py | 52 +++++++++++++++++++ quokka/modules/authors/utils.py | 2 +- .../pure/templates/accounts/profile.html | 0 .../pure/templates/accounts/profile_edit.html | 33 ++++++++++++ 8 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 quokka/themes/pure/templates/accounts/profile.html create mode 100644 quokka/themes/pure/templates/accounts/profile_edit.html diff --git a/quokka/core/__init__.py b/quokka/core/__init__.py index d3e4077ce..84b2307ee 100644 --- a/quokka/core/__init__.py +++ b/quokka/core/__init__.py @@ -1,3 +1,7 @@ # -*- coding: utf-8 -*- -TEXT_FORMATS = ("html", "markdown", "plaintext") +TEXT_FORMATS = ( + ("html", "html"), + ("markdown", "markdown"), + ("plaintext", "plaintext") +) diff --git a/quokka/core/fields.py b/quokka/core/fields.py index 8055dc3bf..35457784f 100644 --- a/quokka/core/fields.py +++ b/quokka/core/fields.py @@ -120,6 +120,10 @@ def __init__(self, *args, **kwargs): class ListField(fields.ListField): + + validators = [] # should be removed when flask.mongoengine updates + filters = [] # should be removed "" + def __get__(self, *args, **kwargs): value = super(ListField, self).__get__(*args, **kwargs) inject(value) diff --git a/quokka/modules/accounts/__init__.py b/quokka/modules/accounts/__init__.py index af6dea8f8..7afe9bcff 100644 --- a/quokka/modules/accounts/__init__.py +++ b/quokka/modules/accounts/__init__.py @@ -1,8 +1,12 @@ # coding: utf8 from quokka.core.app import QuokkaModule -from .views import SwatchView +from .views import SwatchView, ProfileEditView, ProfileView module = QuokkaModule('accounts', __name__, template_folder='templates') -module.add_url_rule('/set_swatch/', +module.add_url_rule('/accounts/set_swatch/', view_func=SwatchView.as_view('set_swatch')) +module.add_url_rule('/accounts/profile//', + view_func=ProfileView.as_view('profile')) +module.add_url_rule('/accounts/profile/edit/', + view_func=ProfileEditView.as_view('profile_edit')) diff --git a/quokka/modules/accounts/models.py b/quokka/modules/accounts/models.py index 9b417a490..481b2c47c 100644 --- a/quokka/modules/accounts/models.py +++ b/quokka/modules/accounts/models.py @@ -59,7 +59,15 @@ class User(db.DynamicDocument, ThemeChanger, HasCustomValue, UserMixin): tagline = db.StringField(max_length=255) bio = db.StringField() links = db.ListField(db.EmbeddedDocumentField(UserLink)) + + use_avatar_from = db.StringField( + choices=(("gravatar", "gravatar"), ("url", "url"), ("file", "file")), + default='gravatar' + ) gravatar_email = db.EmailField(max_length=255) + avatar_file_path = db.StringField() + avatar_url = db.StringField(max_length=255) + # facebook image should be get from connections @property def summary(self): diff --git a/quokka/modules/accounts/views.py b/quokka/modules/accounts/views.py index de3486ad1..fc8b2c52e 100644 --- a/quokka/modules/accounts/views.py +++ b/quokka/modules/accounts/views.py @@ -3,7 +3,11 @@ from flask import redirect, request, url_for from flask.views import MethodView +from quokka.utils import get_current_user from flask.ext.security import current_user +from flask.ext.mongoengine.wtf import model_form +from quokka.core.templates import render_template +from quokka.modules.accounts.models import User class SwatchView(MethodView): @@ -14,3 +18,51 @@ class SwatchView(MethodView): def post(self): current_user.set_swatch(request.form.get('swatch')) return redirect(url_for('admin.index')) + + +class ProfileView(MethodView): + """ + Show User Profile + """ + + def get(self, user_id): + return render_template('accounts/profile.html') + + +class ProfileEditView(MethodView): + """ + Edit User Profile + """ + + form = model_form( + User, + only=[ + 'name', + 'email', + 'username', + 'tagline', + 'bio', + 'use_avatar_from', + 'avatar_file_path', + 'gravatar_email', + 'avatar_url', + 'links', + ] + ) + + def needs_login(self, **kwargs): + if not current_user.is_authenticated(): + nex = kwargs.get( + 'next', + request.values.get('next', url_for('accounts.profile_edit')) + ) + return redirect(url_for_security('login', next=nex)) + + def get(self): + return self.needs_login() or render_template( + 'accounts/profile_edit.html', + form=self.form(instance=get_current_user()) + ) + + def post(self): + return redirect(url_for('accounts.profile_edit')) diff --git a/quokka/modules/authors/utils.py b/quokka/modules/authors/utils.py index bc22a9136..58f042157 100644 --- a/quokka/modules/authors/utils.py +++ b/quokka/modules/authors/utils.py @@ -35,7 +35,7 @@ def get_author_contents(author): def get_authors(*args, **kwargs): authors = User.objects.filter( roles__in=Role.objects.filter( - name__in=['admin', 'author'], + name__in=['author'], **kwargs ) ) diff --git a/quokka/themes/pure/templates/accounts/profile.html b/quokka/themes/pure/templates/accounts/profile.html new file mode 100644 index 000000000..e69de29bb diff --git a/quokka/themes/pure/templates/accounts/profile_edit.html b/quokka/themes/pure/templates/accounts/profile_edit.html new file mode 100644 index 000000000..8732bc201 --- /dev/null +++ b/quokka/themes/pure/templates/accounts/profile_edit.html @@ -0,0 +1,33 @@ +{% from theme("security/_macros.html") import render_field_with_errors, render_field, render_button %} +{% extends theme("base.html") %} + +{% block content %} +
+ {% include theme('sidebar.html') %} +
+
+ +
+

Edit Profile

+
+
+ {% for field in form %} + + {% if field.type in ['CSRFTokenField', 'HiddenField'] %} + {{ field() }} + {%elif field.type == 'FieldList' %} + {{field()}} + {% else %} + {{ render_field_with_errors(field, class="form-control") }} + {% endif %} + + {% endfor %} + + +
+
+
+
+
+
+{% endblock %} From 3a3b1502b71b833c420620bef40702f1a309dca5 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 3 Dec 2015 19:05:40 -0200 Subject: [PATCH 04/65] Fix missing import --- quokka/modules/accounts/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/quokka/modules/accounts/views.py b/quokka/modules/accounts/views.py index fc8b2c52e..829b10702 100644 --- a/quokka/modules/accounts/views.py +++ b/quokka/modules/accounts/views.py @@ -4,6 +4,7 @@ from flask import redirect, request, url_for from flask.views import MethodView from quokka.utils import get_current_user +from flask.ext.security.utils import url_for_security from flask.ext.security import current_user from flask.ext.mongoengine.wtf import model_form from quokka.core.templates import render_template From affd08b9aedcb0227ba975feefd1dec1db45dd3c Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 4 Dec 2015 20:30:49 -0200 Subject: [PATCH 05/65] added maxfails to test --- Makefile | 2 +- quokka/core/models/__init__.py | 0 quokka/core/models/channel.py | 233 ++++++++++++++++++++++ quokka/core/models/config.py | 51 +++++ quokka/core/models/content.py | 211 ++++++++++++++++++++ quokka/core/models/custom_values.py | 98 +++++++++ quokka/core/models/signature.py | 139 +++++++++++++ quokka/core/models/subcontent.py | 55 +++++ quokka/themes/pure/templates/flashes.html | 11 + 9 files changed, 799 insertions(+), 1 deletion(-) create mode 100644 quokka/core/models/__init__.py create mode 100644 quokka/core/models/channel.py create mode 100644 quokka/core/models/config.py create mode 100644 quokka/core/models/content.py create mode 100644 quokka/core/models/custom_values.py create mode 100644 quokka/core/models/signature.py create mode 100644 quokka/core/models/subcontent.py create mode 100644 quokka/themes/pure/templates/flashes.html diff --git a/Makefile b/Makefile index 3d4940fea..c6cc40fb4 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ shell: .PHONY: test test: pep8 - QUOKKA_MODE=test py.test --cov=quokka quokka/ + QUOKKA_MODE=test py.test --cov=quokka -l --tb=short --maxfail=1 quokka/ .PHONY: install install: diff --git a/quokka/core/models/__init__.py b/quokka/core/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/quokka/core/models/channel.py b/quokka/core/models/channel.py new file mode 100644 index 000000000..60487983b --- /dev/null +++ b/quokka/core/models/channel.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import logging +from flask import request +from flask.ext.mistune import markdown + +from quokka.core.db import db +from quokka.core.models.custom_values import HasCustomValue +from quokka.core.models.signature import ( + Tagged, Publishable, LongSlugged, ContentFormat, TemplateType +) +from quokka.core.models.config import Config +from quokka.core.admin.utils import _l + +logger = logging.getLogger() + + +class ChannelConfigs(object): + content_filters = db.DictField(required=False, default=lambda: {}) + inherit_parent = db.BooleanField(default=True) + + +class ChannelType(TemplateType, ChannelConfigs, db.DynamicDocument): + """Define the channel template type and its filters""" + + +class ContentProxy(db.DynamicDocument): + content = db.GenericReferenceField(required=True, unique=True) + + def __unicode__(self): + return self.content.title + + +class Channel(Tagged, HasCustomValue, Publishable, LongSlugged, + ChannelConfigs, ContentFormat, db.DynamicDocument): + title = db.StringField(max_length=255, required=True) + description = db.StringField() + show_in_menu = db.BooleanField(default=False) + is_homepage = db.BooleanField(default=False) + roles = db.ListField( + db.ReferenceField('Role', reverse_delete_rule=db.PULL)) + include_in_rss = db.BooleanField(default=True) + indexable = db.BooleanField(default=True) + canonical_url = db.StringField() + order = db.IntField(default=0) + + parent = db.ReferenceField('self', required=False, default=None, + reverse_delete_rule=db.DENY) + + per_page = db.IntField(default=0) + aliases = db.ListField(db.StringField(), default=[]) + channel_type = db.ReferenceField(ChannelType, required=False, + reverse_delete_rule=db.NULLIFY) + + redirect_url = db.StringField(max_length=255) + render_content = db.ReferenceField(ContentProxy, + required=False, + reverse_delete_rule=db.NULLIFY) + sort_by = db.ListField(db.StringField(), default=[]) + + meta = { + 'ordering': ['order', 'title'], + } + + def get_text(self): + if self.content_format == "markdown": + return markdown(self.description) + else: + return self.description + + def get_content_filters(self): + filters = {} + if self.channel_type and self.channel_type.content_filters: + filters.update(self.channel_type.content_filters) + if self.content_filters: + filters.update(self.content_filters) + return filters + + def get_ancestors_count(self): + """ + return how many ancestors this node has based on slugs + """ + return len(self.get_ancestors_slugs()) + + def get_ancestors_slugs(self): + """return ancestors slugs including self as 1st item + >>> channel = Channel(long_slug='articles/technology/programming') + >>> channel.get_ancestors_slugs() + ['articles/technology/programming', + 'articles/technology', + 'articles'] + """ + + channel_list = [] + channel_slugs = self.long_slug.split('/') + while channel_slugs: + channel_list.append("/".join(channel_slugs)) + channel_slugs.pop() + return channel_list + + def get_ancestors(self, **kwargs): + """return all ancestors includind self as 1st item""" + channel_list = self.get_ancestors_slugs() + ancestors = self.__class__.objects( + long_slug__in=channel_list, + **kwargs + ).order_by('-long_slug') + return ancestors + + def get_children(self, **kwargs): + """return direct children 1 level depth""" + return self.__class__.objects( + parent=self, **kwargs + ).order_by('long_slug') + + def get_descendants(self, **kwargs): + """return all descendants including self as 1st item""" + return self.__class__.objects( + __raw__={'mpath': {'$regex': '^{0}'.format(self.mpath)}} + ).order_by('long_slug') + + def get_themes(self): + return list({ + c.channel_type.theme_name + for c in self.get_ancestors(channel_type__ne=None) + if c.channel_type and c.channel_type.theme_name + }) + + @classmethod + def get_homepage(cls, attr=None): + try: + homepage = cls.objects.get(is_homepage=True) + except Exception as e: + logger.info("There is no homepage: %s", e.message) + return None + else: + if not attr: + return homepage + else: + return getattr(homepage, attr, homepage) + + def __unicode__(self): + return self.long_slug + + def get_absolute_url(self, *args, **kwargs): + if self.is_homepage: + return "/" + return "/{0}/".format(self.long_slug) + + def get_canonical_url(self, *args, **kwargs): + """ + This method should be reviewed + Canonical URL is the preferred URL for a content + when the content can be served by multiple URLS + In the case of channels it will never happen + until we implement the channel alias feature + """ + if self.is_homepage: + return "/" + return self.get_absolute_url() + + def get_http_url(self): + site_url = Config.get('site', 'site_domain', request.url_root) + return u"{}{}".format(site_url, self.get_absolute_url()) + + def clean(self): + homepage = Channel.objects(is_homepage=True) + if self.is_homepage and homepage and self not in homepage: + raise db.ValidationError(_l("Home page already exists")) + super(Channel, self).clean() + + def validate_render_content(self): + if self.render_content and \ + not isinstance(self.render_content, ContentProxy): + self.render_content, created = ContentProxy.objects.get_or_create( + content=self.render_content) + else: + self.render_content = None + + def heritage(self): + """populate inheritance from parent channels""" + parent = self.parent + if not parent or not self.inherit_parent: + return + + self.content_filters = self.content_filters or parent.content_filters + self.include_in_rss = self.include_in_rss or parent.include_in_rss + self.show_in_menu = self.show_in_menu or parent.show_in_menu + self.indexable = self.indexable or parent.indexable + self.channel_type = self.channel_type or parent.channel_type + + def update_descendants_and_contents(self): + """ + Need to Detect if self.long_slug and self.mpath has changed. + if so, update every descendant using get_descendatns method + to query. + Also update long_slug and mpath for every Content in this channel + This needs to be done by default in araw immediate way, but if + current_app.config.get('ASYNC_SAVE_MODE') is True it will delegate + all those tasks to celery.""" + + def save(self, *args, **kwargs): + self.validate_render_content() + self.validate_slug() + self.validate_long_slug() + self.heritage() + self.update_descendants_and_contents() + if not self.channel_type: + self.channel_type = ChannelType.objects.first() + super(Channel, self).save(*args, **kwargs) + + +class Channeling(object): + channel = db.ReferenceField(Channel, required=True, + reverse_delete_rule=db.DENY) + related_channels = db.ListField( + db.ReferenceField('Channel', reverse_delete_rule=db.PULL) + ) + related_mpath = db.ListField(db.StringField()) + show_on_channel = db.BooleanField(default=True) + channel_roles = db.ListField(db.StringField()) + + def populate_related_mpath(self): + self.related_mpath = [rel.mpath for rel in self.related_channels] + + def populate_channel_roles(self): + self.channel_roles = [role.name for role in self.channel.roles] + + +class ChannelingNotRequired(Channeling): + channel = db.ReferenceField(Channel, required=False, + reverse_delete_rule=db.NULLIFY) diff --git a/quokka/core/models/config.py b/quokka/core/models/config.py new file mode 100644 index 000000000..176387c7e --- /dev/null +++ b/quokka/core/models/config.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import logging +from flask import current_app +from quokka.core.db import db +from quokka.core.fields import MultipleObjectsReturned +from quokka.core.models.custom_values import HasCustomValue +from quokka.core.models.signature import ( + Publishable, ContentFormat, Dated, Slugged +) + +logger = logging.getLogger() + + +class Config(HasCustomValue, ContentFormat, Publishable, db.DynamicDocument): + group = db.StringField(max_length=255) + description = db.StringField() + + @classmethod + def get(cls, group, name=None, default=None): + + try: + instance = cls.objects.get(group=group) + except: + return None + + if not name: + ret = instance.values + if group == 'settings': + ret = {} + ret.update(current_app.config) + ret.update({item.name: item.value for item in instance.values}) + else: + try: + ret = instance.values.get(name=name).value + except (MultipleObjectsReturned, AttributeError): + ret = None + + if not ret and group == 'settings' and name is not None: + # get direct from store to avoid infinite loop + ret = current_app.config.store.get(name) + + return ret or default + + def __unicode__(self): + return self.group + + +class Quokka(Dated, Slugged, db.DynamicDocument): + """ Hidden collection for installation control""" diff --git a/quokka/core/models/content.py b/quokka/core/models/content.py new file mode 100644 index 000000000..f6972f15d --- /dev/null +++ b/quokka/core/models/content.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import logging +import datetime +from flask import url_for, redirect +from flask.ext.mistune import markdown + +from quokka.core.db import db +from quokka.utils.shorturl import ShorterURL +from quokka.utils.settings import get_setting_value, get_site_url +from quokka.core.models.channel import Channeling +from quokka.core.models.custom_values import HasCustomValue +from quokka.core.models.subcontent import SubContent +from quokka.core.models.signature import ( + Tagged, Publishable, LongSlugged, ContentFormat, TemplateType +) + +logger = logging.getLogger() + + +class ContentTemplateType(TemplateType, db.Document): + """Define the content template type and its theme""" + + +class License(db.EmbeddedDocument): + LICENSES = (('custom', 'custom'), + ('creative_commons_by_nc_nd', 'creative_commons_by_nc_nd')) + title = db.StringField(max_length=255) + link = db.StringField(max_length=255) + identifier = db.StringField(max_length=255, choices=LICENSES) + + +class ShortenedURL(db.EmbeddedDocument): + original = db.StringField(max_length=255) + short = db.StringField(max_length=255) + + def __str__(self): + return self.short + + +############################################################### +# Base Content for every new content to extend. inheritance=True +############################################################### + + +class Content(HasCustomValue, Publishable, LongSlugged, + Channeling, Tagged, ContentFormat, db.DynamicDocument): + title = db.StringField(max_length=255, required=True) + summary = db.StringField(required=False) + template_type = db.ReferenceField(ContentTemplateType, + required=False, + reverse_delete_rule=db.NULLIFY) + contents = db.ListField(db.EmbeddedDocumentField(SubContent)) + model = db.StringField() + comments_enabled = db.BooleanField(default=True) + license = db.EmbeddedDocumentField(License) + shortened_url = db.EmbeddedDocumentField(ShortenedURL) + + meta = { + 'allow_inheritance': True, + 'indexes': ['-created_at', 'slug'], + 'ordering': ['-created_at'], + } + + @classmethod + def available_objects(cls, **filters): + now = datetime.datetime.now() + default_filters = { + "published": True, + 'available_at__lte': now, + } + default_filters.update(filters) + return cls.objects(**default_filters) + + def get_main_image_url(self, thumb=False, + default=None, identifier='mainimage'): + """ + """ + if not isinstance(identifier, (list, tuple)): + identifier = [identifier] + + for item in identifier: + try: + if not thumb: + path = self.contents.get(identifier=item).content.path + else: + path = self.contents.get(identifier=item).content.thumb + return url_for('media', filename=path) + except Exception as e: + logger.warning('get_main_image_url:' + str(e)) + + return default + + def get_uid(self): + return str(self.id) + + def get_themes(self): + themes = self.channel.get_themes() + theme = self.template_type and self.template_type.theme_name + if theme: + themes.insert(0, theme) + return list(set(themes)) + + def get_http_url(self): + site_url = get_site_url() + absolute_url = self.get_absolute_url() + absolute_url = absolute_url[1:] + return u"{}{}".format(site_url, absolute_url) + + def get_absolute_url(self, endpoint='detail'): + if self.channel.is_homepage: + long_slug = self.slug + else: + long_slug = self.long_slug + + try: + return url_for(self.URL_NAMESPACE, long_slug=long_slug) + except: + return url_for(endpoint, long_slug=long_slug) + + def get_canonical_url(self, *args, **kwargs): + return self.get_absolute_url() + + def get_recommendations(self, limit=3, ordering='-created_at', *a, **k): + now = datetime.datetime.now() + filters = { + 'published': True, + 'available_at__lte': now, + "id__ne": self.id + } + contents = Content.objects(**filters).filter(tags__in=self.tags or []) + + return contents.order_by(ordering)[:limit] + + def get_summary(self): + if self.summary: + return self.summary + return self.get_text() + + def get_text(self): + if hasattr(self, 'body'): + text = self.body + elif hasattr(self, 'description'): + text = self.description + else: + text = self.summary or "" + + if self.content_format == "markdown": + return markdown(text) + else: + return text + + def __unicode__(self): + return self.title + + @property + def short_url(self): + return self.shortened_url.short if self.shortened_url else '' + + @property + def model_name(self): + return self.__class__.__name__.lower() + + @property + def module_name(self): + module = self.__module__ + module_name = module.replace('quokka.modules.', '').split('.')[0] + return module_name + + def heritage(self): + self.model = "{0}.{1}".format(self.module_name, self.model_name) + + def save(self, *args, **kwargs): + # all those functions should be in a dynamic pipeline + self.validate_slug() + self.validate_long_slug() + self.heritage() + self.populate_related_mpath() + self.populate_channel_roles() + self.populate_shorter_url() + super(Content, self).save(*args, **kwargs) + + def pre_render(self, render_function, *args, **kwargs): + return render_function(*args, **kwargs) + + def populate_shorter_url(self): + if not self.published or not get_setting_value('SHORTENER_ENABLED'): + return + + url = self.get_http_url() + if not self.shortened_url or url != self.shortened_url.original: + shortener = ShorterURL() + self.shortened_url = ShortenedURL(original=url, + short=shortener.short(url)) + + +class Link(Content): + link = db.StringField(required=True) + force_redirect = db.BooleanField(default=True) + increment_visits = db.BooleanField(default=True) + visits = db.IntField(default=0) + show_on_channel = db.BooleanField(default=False) + + def pre_render(self, render_function, *args, **kwargs): + if self.increment_visits: + self.visits = self.visits + 1 + self.save() + if self.force_redirect: + return redirect(self.link) + return super(Link, self).pre_render(render_function, *args, **kwargs) diff --git a/quokka/core/models/custom_values.py b/quokka/core/models/custom_values.py new file mode 100644 index 000000000..7739417ca --- /dev/null +++ b/quokka/core/models/custom_values.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import json +from quokka.core.db import db +from quokka.core.admin.utils import _l + + +def default_formatter(value): + return value + + +class CustomValue(db.EmbeddedDocument): + + FORMATS = ( + ('json', "json"), + ('text', "text"), + ('int', "int"), + ('float', "float"), + ) + + DEFAULT_FORMATTER = default_formatter + + FORMATTERS = { + 'json': json.loads, + 'text': DEFAULT_FORMATTER, + 'int': int, + 'float': float + } + + REVERSE_FORMATTERS = { + 'json': lambda val: val if isinstance(val, str) else json.dumps(val), + 'text': DEFAULT_FORMATTER, + 'int': DEFAULT_FORMATTER, + 'float': DEFAULT_FORMATTER + } + + name = db.StringField(max_length=50, required=True) + rawvalue = db.StringField(verbose_name=_l("Value"), + required=True) + formatter = db.StringField(choices=FORMATS, default="text", required=True) + + @property + def value(self): + return self.FORMATTERS.get(self.formatter, + self.DEFAULT_FORMATTER)(self.rawvalue) + + @value.setter + def value(self, value): + self.rawvalue = self.REVERSE_FORMATTERS.get(self.formatter, + self.STR_FORMATTER)(value) + + def clean(self): + try: + self.value + except Exception as e: + # raise base exception because Flask-Admin can't handle the output + # for some specific Exceptions of Mongoengine + raise Exception(e.message) + super(CustomValue, self).clean() + + def __unicode__(self): + return u"{s.name} -> {s.value}".format(s=self) + + +class HasCustomValue(object): + values = db.ListField(db.EmbeddedDocumentField(CustomValue)) + + def get_values_tuple(self): + return [(value.name, value.value, value.formatter) + for value in self.values] + + def get_value(self, name, default=None): + try: + return self.values.get(name=name).value + except: + return default + + def add_value(self, name, value, formatter='text'): + """ + Another implementation + data = {"name": name, "rawvalue": value, "formatter": formatter} + self.values.update(data, name=name) or self.values.create(**data) + """ + custom_value = CustomValue( + name=name, + value=value, + formatter=formatter + ) + self.values.append(custom_value) + + def clean(self): + current_names = [value.name for value in self.values] + for name in current_names: + if current_names.count(name) > 1: + raise Exception(_l("%(name)s already exists", + name=name)) + super(HasCustomValue, self).clean() diff --git a/quokka/core/models/signature.py b/quokka/core/models/signature.py new file mode 100644 index 000000000..b4eabc006 --- /dev/null +++ b/quokka/core/models/signature.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import datetime +import random +from flask import current_app +from quokka.core import TEXT_FORMATS +from quokka.core.db import db +from quokka.modules.accounts.models import User +from quokka.utils.text import slugify +from quokka.utils import get_current_user_for_models +from quokka.utils.settings import get_setting_value +from quokka.core.admin.utils import _l +from quokka.core.models.custom_values import HasCustomValue + +############################################################### +# Commom extendable base classes +############################################################### + + +class ContentFormat(object): + content_format = db.StringField( + choices=TEXT_FORMATS, + default=get_setting_value('DEFAULT_TEXT_FORMAT', 'html') + ) + + +class Dated(object): + available_at = db.DateTimeField(default=datetime.datetime.now) + available_until = db.DateTimeField(required=False) + created_at = db.DateTimeField(default=datetime.datetime.now) + updated_at = db.DateTimeField(default=datetime.datetime.now) + + +class Owned(object): + created_by = db.ReferenceField(User) + last_updated_by = db.ReferenceField(User) + authors = db.ListField(db.ReferenceField(User)) + + def get_authors(self, sortedf=None): + return set(self.authors + [self.created_by]) + + @property + def has_multiple_authors(self): + return len(self.get_authors()) > 1 + + +class Publishable(Dated, Owned): + published = db.BooleanField(default=False) + + @property + def is_available(self): + now = datetime.datetime.now() + return ( + self.published and + self.available_at <= now and + (self.available_until is None or + self.available_until >= now) + ) + + def save(self, *args, **kwargs): + self.updated_at = datetime.datetime.now() + + user = get_current_user_for_models() + + if not self.id and not self.created_by: + self.created_by = user + self.last_updated_by = user + + super(Publishable, self).save(*args, **kwargs) + + +class Slugged(object): + slug = db.StringField(max_length=255, required=True) + + def validate_slug(self, title=None): + if self.slug: + self.slug = slugify(self.slug) + else: + self.slug = slugify(title or self.title) + + +class LongSlugged(Slugged): + long_slug = db.StringField(unique=True, required=True) + mpath = db.StringField() + + def _create_mpath_long_slug(self): + if hasattr(self, 'is_homepage'): # is channel + if self.parent and self.parent != self: + self.long_slug = "/".join( + [self.parent.long_slug, self.slug] + ) + self.mpath = "".join( + [self.parent.mpath, self.slug, ','] + ) + else: + self.long_slug = self.slug + self.mpath = ",%s," % self.slug + else: # is Content + self.long_slug = "/".join( + [self.channel.long_slug, self.slug] + ) + self.mpath = "".join([self.channel.mpath, self.slug, ',']) + + def validate_long_slug(self): + self._create_mpath_long_slug() + + filters = dict(long_slug=self.long_slug) + if self.id: + filters['id__ne'] = self.id + + exist = self.__class__.objects(**filters) + if exist.count(): + if current_app.config.get('SMART_SLUG_ENABLED', False): + self.slug = "{0}-{1}".format(self.slug, random.getrandbits(32)) + self._create_mpath_long_slug() + else: + raise db.ValidationError( + _l("%(slug)s slug already exists", + slug=self.long_slug) + ) + + +class Tagged(object): + tags = db.ListField(db.StringField(max_length=50)) + + +class Ordered(object): + order = db.IntField(required=True, default=1) + + +class TemplateType(HasCustomValue): + title = db.StringField(max_length=255, required=True) + identifier = db.StringField(max_length=255, required=True, unique=True) + template_suffix = db.StringField(max_length=255, required=True) + theme_name = db.StringField(max_length=255, required=False) + + def __unicode__(self): + return self.title diff --git a/quokka/core/models/subcontent.py b/quokka/core/models/subcontent.py new file mode 100644 index 000000000..2203f50a7 --- /dev/null +++ b/quokka/core/models/subcontent.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import logging +from flask import url_for +from quokka.core.db import db +from quokka.utils.text import slugify +from quokka.core.models.signature import Publishable, Ordered + +logger = logging.getLogger() + + +class SubContentPurpose(db.Document): + title = db.StringField(max_length=255, required=True) + identifier = db.StringField(max_length=255, required=True, unique=True) + module = db.StringField() + + def save(self, *args, **kwargs): + self.identifier = slugify(self.identifier or self.title) + super(SubContentPurpose, self).save(*args, **kwargs) + + def __unicode__(self): + return self.title + + +class SubContent(Publishable, Ordered, db.EmbeddedDocument): + """Content can have inner contents + Its useful for any kind of relation with Content childs + Images, ImageGalleries, RelatedContent, Attachments, Media + """ + + content = db.ReferenceField('Content', required=True) + caption = db.StringField() + purpose = db.ReferenceField(SubContentPurpose, required=True) + identifier = db.StringField() + + @property + def thumb(self): + try: + return url_for('media', filename=self.content.thumb) + # return self.content.thumb + except Exception as e: + logger.warning(str(e)) + return self.content.get_main_image_url(thumb=True) + + meta = { + 'ordering': ['order'], + 'indexes': ['order'] + } + + def clean(self): + self.identifier = self.purpose.identifier + + def __unicode__(self): + return self.content and self.content.title or self.caption diff --git a/quokka/themes/pure/templates/flashes.html b/quokka/themes/pure/templates/flashes.html new file mode 100644 index 000000000..e3c4681d0 --- /dev/null +++ b/quokka/themes/pure/templates/flashes.html @@ -0,0 +1,11 @@ +{% block flash_messages %} + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
    + {% for category, message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +{% endblock %} From 3703a18b5511d7159d577810190faba59bb68496 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 4 Dec 2015 20:31:44 -0200 Subject: [PATCH 06/65] reorganized some model paths --- etc/openshift_clean.py | 4 +++- quokka/core/admin/__init__.py | 14 ++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/etc/openshift_clean.py b/etc/openshift_clean.py index ecc031354..a05d4c5f5 100644 --- a/etc/openshift_clean.py +++ b/etc/openshift_clean.py @@ -5,7 +5,9 @@ OR AT YOUR OWN RISK!!!! """ from quokka import create_app -from quokka.core.models import Content, Channel, Config +from quokka.core.models.content import Content +from quokka.core.models.config import Config +from quokka.core.models.channel import Channel from quokka.modules.accounts.models import User app = create_app() diff --git a/quokka/core/admin/__init__.py b/quokka/core/admin/__init__.py index 9f7018ab0..4ed3900c8 100644 --- a/quokka/core/admin/__init__.py +++ b/quokka/core/admin/__init__.py @@ -3,21 +3,23 @@ import logging from werkzeug.utils import import_string + from flask import request, session from flask.ext.admin import Admin -from ..models import (Link, Config, SubContentPurpose, ChannelType, - ContentTemplateType, Channel) +from quokka.core.models.subcontent import SubContentPurpose +from quokka.core.models.config import Config +from quokka.core.models.channel import Channel, ChannelType +from quokka.core.models.content import Link, ContentTemplateType + +from quokka.utils.translation import _l, _n +from quokka.utils.settings import get_setting_value from .models import ModelAdmin, FileAdmin, BaseIndexView from .views import (IndexView, InspectorView, LinkAdmin, ConfigAdmin, SubContentPurposeAdmin, ChannelTypeAdmin, ContentTemplateTypeAdmin, ChannelAdmin) -from quokka.utils.translation import _l, _n -from quokka.utils.settings import get_setting_value - - ''' _n is here only for backwards compatibility, to be imported by 3rd party modules. The below _n below is to avoid pep8 error From 4f44c3c2040cedd1a5c890def9f104e80113732c Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 4 Dec 2015 20:31:56 -0200 Subject: [PATCH 07/65] avoid registering admin view twice --- quokka/core/admin/__init__.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/quokka/core/admin/__init__.py b/quokka/core/admin/__init__.py index 4ed3900c8..cfa85005b 100644 --- a/quokka/core/admin/__init__.py +++ b/quokka/core/admin/__init__.py @@ -30,20 +30,14 @@ class QuokkaAdmin(Admin): + registered = [] def register(self, model, view=None, *args, **kwargs): _view = view or ModelAdmin admin_view_exclude = get_setting_value('ADMIN_VIEW_EXCLUDE', []) identifier = '.'.join((model.__module__, model.__name__)) - if identifier not in admin_view_exclude: + if identifier not in admin_view_exclude and not identifier in self.registered: self.add_view(_view(model, *args, **kwargs)) - # try: - # self.add_view(_view(model, *args, **kwargs)) - # except Exception as e: - # logger.warning( - # "admin.register({0}, {1}, {2}, {3}) error: {4}".format( - # model, view, args, kwargs, e.message - # ) - # ) + self.registered.append(identifier) def create_admin(app=None): From 5fa2eca49906c5d64cc5d696dae1f35900bb5432 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 4 Dec 2015 20:34:29 -0200 Subject: [PATCH 08/65] base_models replaced by models as module (BREAK COMPATIBILITY!!!) --- quokka/core/admin/fields.py | 2 +- quokka/core/admin/models.py | 4 +- quokka/core/admin/views.py | 2 +- quokka/core/base_models/__init__.py | 0 quokka/core/base_models/custom_values.py | 85 --- quokka/core/models.py | 638 ----------------------- quokka/core/tests/test_models.py | 6 +- quokka/core/views.py | 4 +- quokka/ext/blueprints.py | 15 +- quokka/ext/context_processors.py | 4 +- quokka/ext/fixtures.py | 2 +- quokka/ext/template_filters.py | 2 +- quokka/ext/views.py | 2 +- quokka/modules/accounts/admin.py | 3 +- quokka/modules/accounts/models.py | 4 +- quokka/modules/accounts/views.py | 7 +- quokka/modules/authors/utils.py | 2 +- quokka/modules/comments/models.py | 2 +- quokka/modules/media/admin.py | 2 +- quokka/modules/media/models.py | 3 +- quokka/modules/posts/models.py | 2 +- quokka/modules/posts/tests/test_model.py | 2 +- quokka/tests/test_basic.py | 2 +- quokka/utils/populate.py | 8 +- quokka/utils/settings.py | 4 +- 25 files changed, 49 insertions(+), 758 deletions(-) delete mode 100644 quokka/core/base_models/__init__.py delete mode 100644 quokka/core/base_models/custom_values.py delete mode 100644 quokka/core/models.py diff --git a/quokka/core/admin/fields.py b/quokka/core/admin/fields.py index c84d16170..62dbcbe6a 100644 --- a/quokka/core/admin/fields.py +++ b/quokka/core/admin/fields.py @@ -8,7 +8,7 @@ from flask.ext.admin.form.upload import ImageUploadInput from flask.ext.admin._compat import urljoin -from quokka.core.models import SubContent, SubContentPurpose +from quokka.core.models.subcontent import SubContent, SubContentPurpose from quokka.modules.media.models import Image if sys.version_info.major == 3: diff --git a/quokka/core/admin/models.py b/quokka/core/admin/models.py index 699ce2043..6627ae859 100644 --- a/quokka/core/admin/models.py +++ b/quokka/core/admin/models.py @@ -23,9 +23,9 @@ from quokka.utils.upload import dated_path, lazy_media_path from quokka.utils import is_accessible from quokka.utils.settings import get_setting_value -from .fields import ThumbField -from .utils import _, _l, _n +from quokka.core.admin.fields import ThumbField +from quokka.core.admin.utils import _, _l, _n class ThemeMixin(object): diff --git a/quokka/core/admin/views.py b/quokka/core/admin/views.py index 7c866423c..a598416c9 100644 --- a/quokka/core/admin/views.py +++ b/quokka/core/admin/views.py @@ -2,7 +2,7 @@ from flask import current_app, flash from flask.ext.admin.actions import action -from quokka.core.models import Content +from quokka.core.models.content import Content from quokka.utils.routing import expose from quokka.core.widgets import TextEditor, PrepopulatedText from .utils import _, _l diff --git a/quokka/core/base_models/__init__.py b/quokka/core/base_models/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/quokka/core/base_models/custom_values.py b/quokka/core/base_models/custom_values.py deleted file mode 100644 index af932a954..000000000 --- a/quokka/core/base_models/custom_values.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import json -from quokka.core.db import db -from quokka.core.admin.utils import _l - - -def default_formatter(value): - return value - - -class CustomValue(db.EmbeddedDocument): - - FORMATS = ( - ('json', "json"), - ('text', "text"), - ('int', "int"), - ('float', "float"), - ) - - DEFAULT_FORMATTER = default_formatter - - FORMATTERS = { - 'json': json.loads, - 'text': DEFAULT_FORMATTER, - 'int': int, - 'float': float - } - - REVERSE_FORMATTERS = { - 'json': lambda val: val if isinstance(val, str) else json.dumps(val), - 'text': DEFAULT_FORMATTER, - 'int': DEFAULT_FORMATTER, - 'float': DEFAULT_FORMATTER - } - - name = db.StringField(max_length=50, required=True) - rawvalue = db.StringField(verbose_name=_l("Value"), - required=True) - formatter = db.StringField(choices=FORMATS, default="text", required=True) - - @property - def value(self): - return self.FORMATTERS.get(self.formatter, - self.DEFAULT_FORMATTER)(self.rawvalue) - - @value.setter - def value(self, value): - self.rawvalue = self.REVERSE_FORMATTERS.get(self.formatter, - self.STR_FORMATTER)(value) - - def clean(self): - try: - self.value - except Exception as e: - # raise base exception because Flask-Admin can't handle the output - # for some specific Exceptions of Mongoengine - raise Exception(e.message) - super(CustomValue, self).clean() - - def __unicode__(self): - return u"{s.name} -> {s.value}".format(s=self) - - -class HasCustomValue(object): - values = db.ListField(db.EmbeddedDocumentField(CustomValue)) - - def get_values_tuple(self): - return [(value.name, value.value, value.formatter) - for value in self.values] - - def get_value(self, name, default=None): - try: - return self.values.get(name=name).value - except: - return default - - def clean(self): - current_names = [value.name for value in self.values] - for name in current_names: - if current_names.count(name) > 1: - raise Exception(_l("%(name)s already exists", - name=name)) - super(HasCustomValue, self).clean() diff --git a/quokka/core/models.py b/quokka/core/models.py deleted file mode 100644 index ceb657385..000000000 --- a/quokka/core/models.py +++ /dev/null @@ -1,638 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import logging -import datetime -import random -from flask import url_for, current_app, redirect, request -from flask.ext.mistune import markdown - -from quokka.core import TEXT_FORMATS -from quokka.core.db import db -from quokka.core.fields import MultipleObjectsReturned -from quokka.modules.accounts.models import User -from quokka.utils.text import slugify -from quokka.utils import get_current_user_for_models -from quokka.utils.shorturl import ShorterURL -from quokka.utils.settings import get_setting_value, get_site_url -from .base_models.custom_values import HasCustomValue -from .admin.utils import _l - -logger = logging.getLogger() - - -############################################################### -# Commom extendable base classes -############################################################### - -class ContentFormat(object): - content_format = db.StringField( - choices=TEXT_FORMATS, - default=get_setting_value('DEFAULT_TEXT_FORMAT', 'html') - ) - - -class Dated(object): - available_at = db.DateTimeField(default=datetime.datetime.now) - available_until = db.DateTimeField(required=False) - created_at = db.DateTimeField(default=datetime.datetime.now) - updated_at = db.DateTimeField(default=datetime.datetime.now) - - -class Owned(object): - created_by = db.ReferenceField(User) - last_updated_by = db.ReferenceField(User) - authors = db.ListField(db.ReferenceField(User)) - - def get_authors(self, sortedf=None): - return set(self.authors + [self.created_by]) - - @property - def has_multiple_authors(self): - return len(self.get_authors()) > 1 - - -class Publishable(Dated, Owned): - published = db.BooleanField(default=False) - - @property - def is_available(self): - now = datetime.datetime.now() - return ( - self.published and - self.available_at <= now and - (self.available_until is None or - self.available_until >= now) - ) - - def save(self, *args, **kwargs): - self.updated_at = datetime.datetime.now() - - user = get_current_user_for_models() - - if not self.id and not self.created_by: - self.created_by = user - self.last_updated_by = user - - super(Publishable, self).save(*args, **kwargs) - - -class Slugged(object): - slug = db.StringField(max_length=255, required=True) - - def validate_slug(self, title=None): - if self.slug: - self.slug = slugify(self.slug) - else: - self.slug = slugify(title or self.title) - - -class LongSlugged(Slugged): - long_slug = db.StringField(unique=True, required=True) - mpath = db.StringField() - - def _create_mpath_long_slug(self): - if isinstance(self, Channel): - if self.parent and self.parent != self: - self.long_slug = "/".join( - [self.parent.long_slug, self.slug] - ) - self.mpath = "".join( - [self.parent.mpath, self.slug, ','] - ) - else: - self.long_slug = self.slug - self.mpath = ",%s," % self.slug - elif isinstance(self, Content): - self.long_slug = "/".join( - [self.channel.long_slug, self.slug] - ) - self.mpath = "".join([self.channel.mpath, self.slug, ',']) - - def validate_long_slug(self): - self._create_mpath_long_slug() - - filters = dict(long_slug=self.long_slug) - if self.id: - filters['id__ne'] = self.id - - exist = self.__class__.objects(**filters) - if exist.count(): - if current_app.config.get('SMART_SLUG_ENABLED', False): - self.slug = "{0}-{1}".format(self.slug, random.getrandbits(32)) - self._create_mpath_long_slug() - else: - raise db.ValidationError( - _l("%(slug)s slug already exists", - slug=self.long_slug) - ) - - -class Tagged(object): - tags = db.ListField(db.StringField(max_length=50)) - - -class Ordered(object): - order = db.IntField(required=True, default=1) - - -class ChannelConfigs(object): - content_filters = db.DictField(required=False, default=lambda: {}) - inherit_parent = db.BooleanField(default=True) - - -class TemplateType(HasCustomValue): - title = db.StringField(max_length=255, required=True) - identifier = db.StringField(max_length=255, required=True, unique=True) - template_suffix = db.StringField(max_length=255, required=True) - theme_name = db.StringField(max_length=255, required=False) - - def __unicode__(self): - return self.title - - -class ChannelType(TemplateType, ChannelConfigs, db.DynamicDocument): - """Define the channel template type and its filters""" - - -class ContentProxy(db.DynamicDocument): - content = db.GenericReferenceField(required=True, unique=True) - - def __unicode__(self): - return self.content.title - - -class Channel(Tagged, HasCustomValue, Publishable, LongSlugged, - ChannelConfigs, ContentFormat, db.DynamicDocument): - title = db.StringField(max_length=255, required=True) - description = db.StringField() - show_in_menu = db.BooleanField(default=False) - is_homepage = db.BooleanField(default=False) - roles = db.ListField( - db.ReferenceField('Role', reverse_delete_rule=db.PULL)) - include_in_rss = db.BooleanField(default=True) - indexable = db.BooleanField(default=True) - canonical_url = db.StringField() - order = db.IntField(default=0) - - parent = db.ReferenceField('self', required=False, default=None, - reverse_delete_rule=db.DENY) - - per_page = db.IntField(default=0) - aliases = db.ListField(db.StringField(), default=[]) - channel_type = db.ReferenceField(ChannelType, required=False, - reverse_delete_rule=db.NULLIFY) - - redirect_url = db.StringField(max_length=255) - render_content = db.ReferenceField(ContentProxy, - required=False, - reverse_delete_rule=db.NULLIFY) - sort_by = db.ListField(db.StringField(), default=[]) - - meta = { - 'ordering': ['order', 'title'], - } - - def get_text(self): - if self.content_format == "markdown": - return markdown(self.description) - else: - return self.description - - def get_content_filters(self): - filters = {} - if self.channel_type and self.channel_type.content_filters: - filters.update(self.channel_type.content_filters) - if self.content_filters: - filters.update(self.content_filters) - return filters - - def get_ancestors_count(self): - """ - return how many ancestors this node has based on slugs - """ - return len(self.get_ancestors_slugs()) - - def get_ancestors_slugs(self): - """return ancestors slugs including self as 1st item - >>> channel = Channel(long_slug='articles/technology/programming') - >>> channel.get_ancestors_slugs() - ['articles/technology/programming', - 'articles/technology', - 'articles'] - """ - - channel_list = [] - channel_slugs = self.long_slug.split('/') - while channel_slugs: - channel_list.append("/".join(channel_slugs)) - channel_slugs.pop() - return channel_list - - def get_ancestors(self, **kwargs): - """return all ancestors includind self as 1st item""" - channel_list = self.get_ancestors_slugs() - ancestors = self.__class__.objects( - long_slug__in=channel_list, - **kwargs - ).order_by('-long_slug') - return ancestors - - def get_children(self, **kwargs): - """return direct children 1 level depth""" - return self.__class__.objects( - parent=self, **kwargs - ).order_by('long_slug') - - def get_descendants(self, **kwargs): - """return all descendants including self as 1st item""" - return self.__class__.objects( - __raw__={'mpath': {'$regex': '^{0}'.format(self.mpath)}} - ).order_by('long_slug') - - def get_themes(self): - return list({ - c.channel_type.theme_name - for c in self.get_ancestors(channel_type__ne=None) - if c.channel_type and c.channel_type.theme_name - }) - - @classmethod - def get_homepage(cls, attr=None): - try: - homepage = cls.objects.get(is_homepage=True) - except Exception as e: - logger.info("There is no homepage: %s", e.message) - return None - else: - if not attr: - return homepage - else: - return getattr(homepage, attr, homepage) - - def __unicode__(self): - return self.long_slug - - def get_absolute_url(self, *args, **kwargs): - if self.is_homepage: - return "/" - return "/{0}/".format(self.long_slug) - - def get_canonical_url(self, *args, **kwargs): - """ - This method should be reviewed - Canonical URL is the preferred URL for a content - when the content can be served by multiple URLS - In the case of channels it will never happen - until we implement the channel alias feature - """ - if self.is_homepage: - return "/" - return self.get_absolute_url() - - def get_http_url(self): - site_url = Config.get('site', 'site_domain', request.url_root) - return u"{}{}".format(site_url, self.get_absolute_url()) - - def clean(self): - homepage = Channel.objects(is_homepage=True) - if self.is_homepage and homepage and self not in homepage: - raise db.ValidationError(_l("Home page already exists")) - super(Channel, self).clean() - - def validate_render_content(self): - if self.render_content and \ - not isinstance(self.render_content, ContentProxy): - self.render_content, created = ContentProxy.objects.get_or_create( - content=self.render_content) - else: - self.render_content = None - - def heritage(self): - """populate inheritance from parent channels""" - parent = self.parent - if not parent or not self.inherit_parent: - return - - self.content_filters = self.content_filters or parent.content_filters - self.include_in_rss = self.include_in_rss or parent.include_in_rss - self.show_in_menu = self.show_in_menu or parent.show_in_menu - self.indexable = self.indexable or parent.indexable - self.channel_type = self.channel_type or parent.channel_type - - def update_descendants_and_contents(self): - """ - Need to Detect if self.long_slug and self.mpath has changed. - if so, update every descendant using get_descendatns method - to query. - Also update long_slug and mpath for every Content in this channel - This needs to be done by default in araw immediate way, but if - current_app.config.get('ASYNC_SAVE_MODE') is True it will delegate - all those tasks to celery.""" - - def save(self, *args, **kwargs): - self.validate_render_content() - self.validate_slug() - self.validate_long_slug() - self.heritage() - self.update_descendants_and_contents() - if not self.channel_type: - self.channel_type = ChannelType.objects.first() - super(Channel, self).save(*args, **kwargs) - - -class Channeling(object): - channel = db.ReferenceField(Channel, required=True, - reverse_delete_rule=db.DENY) - related_channels = db.ListField( - db.ReferenceField('Channel', reverse_delete_rule=db.PULL) - ) - related_mpath = db.ListField(db.StringField()) - show_on_channel = db.BooleanField(default=True) - channel_roles = db.ListField(db.StringField()) - - def populate_related_mpath(self): - self.related_mpath = [rel.mpath for rel in self.related_channels] - - def populate_channel_roles(self): - self.channel_roles = [role.name for role in self.channel.roles] - - -class ChannelingNotRequired(Channeling): - channel = db.ReferenceField(Channel, required=False, - reverse_delete_rule=db.NULLIFY) - - -class Config(HasCustomValue, ContentFormat, Publishable, db.DynamicDocument): - group = db.StringField(max_length=255) - description = db.StringField() - - @classmethod - def get(cls, group, name=None, default=None): - - try: - instance = cls.objects.get(group=group) - except: - return None - - if not name: - ret = instance.values - if group == 'settings': - ret = {} - ret.update(current_app.config) - ret.update({item.name: item.value for item in instance.values}) - else: - try: - ret = instance.values.get(name=name).value - except (MultipleObjectsReturned, AttributeError): - ret = None - - if not ret and group == 'settings' and name is not None: - # get direct from store to avoid infinite loop - ret = current_app.config.store.get(name) - - return ret or default - - def __unicode__(self): - return self.group - - -class Quokka(Dated, Slugged, db.DynamicDocument): - """ Hidden collection """ - - -class ContentTemplateType(TemplateType, db.Document): - """Define the content template type and its theme""" - - -class SubContentPurpose(db.Document): - title = db.StringField(max_length=255, required=True) - identifier = db.StringField(max_length=255, required=True, unique=True) - module = db.StringField() - - def save(self, *args, **kwargs): - self.identifier = slugify(self.identifier or self.title) - super(SubContentPurpose, self).save(*args, **kwargs) - - def __unicode__(self): - return self.title - - -class SubContent(Publishable, Ordered, db.EmbeddedDocument): - """Content can have inner contents - Its useful for any kind of relation with Content childs - Images, ImageGalleries, RelatedContent, Attachments, Media - """ - - content = db.ReferenceField('Content', required=True) - caption = db.StringField() - purpose = db.ReferenceField(SubContentPurpose, required=True) - identifier = db.StringField() - - @property - def thumb(self): - try: - return url_for('media', filename=self.content.thumb) - # return self.content.thumb - except Exception as e: - logger.warning(str(e)) - return self.content.get_main_image_url(thumb=True) - - meta = { - 'ordering': ['order'], - 'indexes': ['order'] - } - - def clean(self): - self.identifier = self.purpose.identifier - - def __unicode__(self): - return self.content and self.content.title or self.caption - - -class License(db.EmbeddedDocument): - LICENSES = (('custom', 'custom'), - ('creative_commons_by_nc_nd', 'creative_commons_by_nc_nd')) - title = db.StringField(max_length=255) - link = db.StringField(max_length=255) - identifier = db.StringField(max_length=255, choices=LICENSES) - - -class ShortenedURL(db.EmbeddedDocument): - original = db.StringField(max_length=255) - short = db.StringField(max_length=255) - - def __str__(self): - return self.short - - -############################################################### -# Base Content for every new content to extend. inheritance=True -############################################################### - - -class Content(HasCustomValue, Publishable, LongSlugged, - Channeling, Tagged, ContentFormat, db.DynamicDocument): - title = db.StringField(max_length=255, required=True) - summary = db.StringField(required=False) - template_type = db.ReferenceField(ContentTemplateType, - required=False, - reverse_delete_rule=db.NULLIFY) - contents = db.ListField(db.EmbeddedDocumentField(SubContent)) - model = db.StringField() - comments_enabled = db.BooleanField(default=True) - license = db.EmbeddedDocumentField(License) - shortened_url = db.EmbeddedDocumentField(ShortenedURL) - - meta = { - 'allow_inheritance': True, - 'indexes': ['-created_at', 'slug'], - 'ordering': ['-created_at'], - } - - @classmethod - def available_objects(cls, **filters): - now = datetime.datetime.now() - default_filters = { - "published": True, - 'available_at__lte': now, - } - default_filters.update(filters) - return cls.objects(**default_filters) - - def get_main_image_url(self, thumb=False, - default=None, identifier='mainimage'): - """ - """ - if not isinstance(identifier, (list, tuple)): - identifier = [identifier] - - for item in identifier: - try: - if not thumb: - path = self.contents.get(identifier=item).content.path - else: - path = self.contents.get(identifier=item).content.thumb - return url_for('media', filename=path) - except Exception as e: - logger.warning('get_main_image_url:' + str(e)) - - return default - - def get_uid(self): - return str(self.id) - - def get_themes(self): - themes = self.channel.get_themes() - theme = self.template_type and self.template_type.theme_name - if theme: - themes.insert(0, theme) - return list(set(themes)) - - def get_http_url(self): - site_url = get_site_url() - absolute_url = self.get_absolute_url() - absolute_url = absolute_url[1:] - return u"{}{}".format(site_url, absolute_url) - - def get_absolute_url(self, endpoint='detail'): - if self.channel.is_homepage: - long_slug = self.slug - else: - long_slug = self.long_slug - - try: - return url_for(self.URL_NAMESPACE, long_slug=long_slug) - except: - return url_for(endpoint, long_slug=long_slug) - - def get_canonical_url(self, *args, **kwargs): - return self.get_absolute_url() - - def get_recommendations(self, limit=3, ordering='-created_at', *a, **k): - now = datetime.datetime.now() - filters = { - 'published': True, - 'available_at__lte': now, - "id__ne": self.id - } - contents = Content.objects(**filters).filter(tags__in=self.tags or []) - - return contents.order_by(ordering)[:limit] - - def get_summary(self): - if self.summary: - return self.summary - return self.get_text() - - def get_text(self): - if hasattr(self, 'body'): - text = self.body - elif hasattr(self, 'description'): - text = self.description - else: - text = self.summary or "" - - if self.content_format == "markdown": - return markdown(text) - else: - return text - - def __unicode__(self): - return self.title - - @property - def short_url(self): - return self.shortened_url.short if self.shortened_url else '' - - @property - def model_name(self): - return self.__class__.__name__.lower() - - @property - def module_name(self): - module = self.__module__ - module_name = module.replace('quokka.modules.', '').split('.')[0] - return module_name - - def heritage(self): - self.model = "{0}.{1}".format(self.module_name, self.model_name) - - def save(self, *args, **kwargs): - # all those functions should be in a dynamic pipeline - self.validate_slug() - self.validate_long_slug() - self.heritage() - self.populate_related_mpath() - self.populate_channel_roles() - self.populate_shorter_url() - super(Content, self).save(*args, **kwargs) - - def pre_render(self, render_function, *args, **kwargs): - return render_function(*args, **kwargs) - - def populate_shorter_url(self): - if not self.published or not get_setting_value('SHORTENER_ENABLED'): - return - - url = self.get_http_url() - if not self.shortened_url or url != self.shortened_url.original: - shortener = ShorterURL() - self.shortened_url = ShortenedURL(original=url, - short=shortener.short(url)) - - -class Link(Content): - link = db.StringField(required=True) - force_redirect = db.BooleanField(default=True) - increment_visits = db.BooleanField(default=True) - visits = db.IntField(default=0) - show_on_channel = db.BooleanField(default=False) - - def pre_render(self, render_function, *args, **kwargs): - if self.increment_visits: - self.visits = self.visits + 1 - self.save() - if self.force_redirect: - return redirect(self.link) - return super(Link, self).pre_render(render_function, *args, **kwargs) diff --git a/quokka/core/tests/test_models.py b/quokka/core/tests/test_models.py index 986adf25e..0c6d12899 100644 --- a/quokka/core/tests/test_models.py +++ b/quokka/core/tests/test_models.py @@ -2,9 +2,9 @@ import sys from . import BaseTestCase - -from ..models import Channel, Config -from ..base_models.custom_values import CustomValue +from quokka.core.models.channel import Channel +from quokka.core.models.config import Config +from quokka.core.models.custom_values import CustomValue if sys.version_info.major == 3: unicode = lambda x: u'{}'.format(x) # flake8: noqa # noqa diff --git a/quokka/core/views.py b/quokka/core/views.py index 7c11d56e4..5118982e4 100644 --- a/quokka/core/views.py +++ b/quokka/core/views.py @@ -10,7 +10,9 @@ from flask import request, redirect, url_for, abort, current_app from flask.views import MethodView from quokka.utils.atom import AtomFeed -from quokka.core.models import Channel, Content, Config +from quokka.core.models.channel import Channel +from quokka.core.models.config import Config +from quokka.core.models.content import Content from quokka.core.templates import render_template from quokka.utils import is_accessible, get_current_user diff --git a/quokka/ext/blueprints.py b/quokka/ext/blueprints.py index 5a8d2143a..b95651213 100644 --- a/quokka/ext/blueprints.py +++ b/quokka/ext/blueprints.py @@ -2,9 +2,7 @@ import os import importlib import random -import logging -from .commands_collector import CommandsCollector -logger = logging.getLogger() +from quokka.ext.commands_collector import CommandsCollector def load_from_packages(app): @@ -43,11 +41,12 @@ def load_from_folder(app): blueprint = getattr(mods[fname], object_name) if blueprint.name not in app.blueprints: + app.logger.info("registering blueprint: %s" % blueprint.name) app.register_blueprint(blueprint) else: blueprint.name += str(random.getrandbits(8)) app.register_blueprint(blueprint) - logger.warning( + app.logger.warning( "CONFLICT:%s already registered, using %s", fname, blueprint.name @@ -56,10 +55,12 @@ def load_from_folder(app): # register admin try: importlib.import_module(".".join([module_name, 'admin'])) - except ImportError: - logger.info("%s module does not define admin", fname) + except ImportError as e: + app.logger.info( + "%s module does not define admin or error: %s", fname, e + ) - logger.info("%s modules loaded", mods.keys()) + app.logger.info("%s modules loaded", mods.keys()) def blueprint_commands(app): diff --git a/quokka/ext/context_processors.py b/quokka/ext/context_processors.py index 0038fdfc5..2d274b5a5 100644 --- a/quokka/ext/context_processors.py +++ b/quokka/ext/context_processors.py @@ -1,7 +1,9 @@ # coding: utf-8 import datetime -from quokka.core.models import Channel, Config, Content, Link +from quokka.core.models.channel import Channel +from quokka.core.models.config import Config +from quokka.core.models.content import Content, Link def configure(app): diff --git a/quokka/ext/fixtures.py b/quokka/ext/fixtures.py index e86fa267e..30f2931bd 100644 --- a/quokka/ext/fixtures.py +++ b/quokka/ext/fixtures.py @@ -1,6 +1,6 @@ # coding: utf-8 from quokka.utils.populate import Populate -from quokka.core.models import Quokka +from quokka.core.models.config import Quokka def configure(app, db): diff --git a/quokka/ext/template_filters.py b/quokka/ext/template_filters.py index 2808ea6b4..31b4bdc80 100644 --- a/quokka/ext/template_filters.py +++ b/quokka/ext/template_filters.py @@ -7,7 +7,7 @@ from flask import Blueprint from werkzeug.routing import Rule from quokka.core.app import QuokkaModule -from quokka.core.models import Content +from quokka.core.models.content import Content from quokka_themes import Theme from pymongo.mongo_client import MongoClient diff --git a/quokka/ext/views.py b/quokka/ext/views.py index aebe3df2a..7bbe3b1be 100644 --- a/quokka/ext/views.py +++ b/quokka/ext/views.py @@ -10,7 +10,7 @@ TagList ) from quokka.core.views import TagAtom, FeedAtom, TagRss, FeedRss -from quokka.core.models import Channel +from quokka.core.models.channel import Channel @roles_accepted('admin', 'developer') diff --git a/quokka/modules/accounts/admin.py b/quokka/modules/accounts/admin.py index 9d7b4b759..4635989a8 100644 --- a/quokka/modules/accounts/admin.py +++ b/quokka/modules/accounts/admin.py @@ -3,9 +3,10 @@ from wtforms.widgets import PasswordInput from quokka import admin from quokka.core.admin.models import ModelAdmin -from .models import Role, User, Connection from quokka.utils.translation import _l +from .models import Role, User, Connection + class UserAdmin(ModelAdmin): roles_accepted = ('admin',) diff --git a/quokka/modules/accounts/models.py b/quokka/modules/accounts/models.py index 481b2c47c..ddb74957c 100644 --- a/quokka/modules/accounts/models.py +++ b/quokka/modules/accounts/models.py @@ -2,11 +2,13 @@ # -*- coding: utf-8 -*- from random import randint +from flask import flash from quokka.core.db import db -from quokka.core.base_models.custom_values import HasCustomValue +from quokka.core.models.custom_values import HasCustomValue from quokka.utils.text import abbreviate, slugify from flask.ext.security import UserMixin, RoleMixin from flask.ext.security.utils import encrypt_password +from flask_gravatar import Gravatar from .utils import ThemeChanger diff --git a/quokka/modules/accounts/views.py b/quokka/modules/accounts/views.py index 829b10702..477793ef9 100644 --- a/quokka/modules/accounts/views.py +++ b/quokka/modules/accounts/views.py @@ -1,14 +1,17 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from flask import redirect, request, url_for +from flask import redirect, request, url_for, flash from flask.views import MethodView from quokka.utils import get_current_user from flask.ext.security.utils import url_for_security from flask.ext.security import current_user from flask.ext.mongoengine.wtf import model_form from quokka.core.templates import render_template -from quokka.modules.accounts.models import User +from .models import User + +# from quokka.core.admin.fields import ImageUploadField +# from quokka.utils.upload import dated_path, lazy_media_path class SwatchView(MethodView): diff --git a/quokka/modules/authors/utils.py b/quokka/modules/authors/utils.py index 58f042157..09ea9ada6 100644 --- a/quokka/modules/authors/utils.py +++ b/quokka/modules/authors/utils.py @@ -2,7 +2,7 @@ import datetime from flask import current_app, request -from quokka.core.models import Content +from quokka.core.models.content import Content from quokka.modules.accounts.models import User, Role diff --git a/quokka/modules/comments/models.py b/quokka/modules/comments/models.py index 3577d3b15..33e804cb3 100644 --- a/quokka/modules/comments/models.py +++ b/quokka/modules/comments/models.py @@ -1,7 +1,7 @@ # coding: utf-8 import uuid from quokka.core.db import db -from quokka.core.models import Publishable +from quokka.core.models.signature import Publishable from quokka.utils.settings import get_setting_value diff --git a/quokka/modules/media/admin.py b/quokka/modules/media/admin.py index e1e7ca9fd..a171f8b3c 100644 --- a/quokka/modules/media/admin.py +++ b/quokka/modules/media/admin.py @@ -6,7 +6,7 @@ from quokka import admin from quokka.utils.settings import get_setting_value -from quokka.core.models import Channel +from quokka.core.models.channel import Channel from quokka.core.admin.models import ModelAdmin from quokka.core.admin.fields import ImageUploadField from quokka.utils.upload import dated_path, lazy_media_path diff --git a/quokka/modules/media/models.py b/quokka/modules/media/models.py index 707c566fb..e1e56b8d1 100644 --- a/quokka/modules/media/models.py +++ b/quokka/modules/media/models.py @@ -2,7 +2,8 @@ import logging from quokka.core.db import db -from quokka.core.models import Content, Channel +from quokka.core.models.channel import Channel +from quokka.core.models.content import Content from flask.ext.admin import form from .controller import MediaController diff --git a/quokka/modules/posts/models.py b/quokka/modules/posts/models.py index 82376af4c..58c4d2a9c 100644 --- a/quokka/modules/posts/models.py +++ b/quokka/modules/posts/models.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- from quokka.core.db import db -from quokka.core.models import Content +from quokka.core.models.content import Content class Post(Content): diff --git a/quokka/modules/posts/tests/test_model.py b/quokka/modules/posts/tests/test_model.py index fa52bf4c8..6a38c49c9 100644 --- a/quokka/modules/posts/tests/test_model.py +++ b/quokka/modules/posts/tests/test_model.py @@ -2,7 +2,7 @@ import sys from quokka.core.tests import BaseTestCase -from quokka.core.models import Channel +from quokka.core.models.channel import Channel from ..models import Post diff --git a/quokka/tests/test_basic.py b/quokka/tests/test_basic.py index c024085e9..97b019cbe 100644 --- a/quokka/tests/test_basic.py +++ b/quokka/tests/test_basic.py @@ -64,7 +64,7 @@ def test_blog_including_related_has_only_3_posts(self): self.assertTrue(Post.objects(**only_blog_filter).count() == 3) def test_has_default_theme(self): - from quokka.core.models import Config + from quokka.core.models.config import Config self.assertTrue(Config.get('settings', 'DEFAULT_THEME') == 'pure') def test_app_has_admin(self): diff --git a/quokka/utils/populate.py b/quokka/utils/populate.py index 5a34dece9..de7580fb0 100644 --- a/quokka/utils/populate.py +++ b/quokka/utils/populate.py @@ -3,9 +3,11 @@ import json import uuid -from quokka.core.models import Channel, ChannelType, SubContentPurpose, \ - Config, License -from quokka.core.base_models.custom_values import CustomValue +from quokka.core.models.subcontent import SubContentPurpose +from quokka.core.models.channel import Channel, ChannelType +from quokka.core.models.config import Config +from quokka.core.models.content import License +from quokka.core.models.custom_values import CustomValue from quokka.modules.accounts.models import User, Role from quokka.modules.posts.models import Post diff --git a/quokka/utils/settings.py b/quokka/utils/settings.py index c4bc8019b..8ec90bcd1 100644 --- a/quokka/utils/settings.py +++ b/quokka/utils/settings.py @@ -1,3 +1,4 @@ +import quokka.core.models as m from flask import current_app, request from quokka.core.db import db from quokka.core.app import QuokkaApp @@ -10,9 +11,8 @@ def create_app_min(config=None, test=False): def get_site_url(): - from quokka.core.models import Config try: - from_site_config = Config.get('site', 'site_domain', None) + from_site_config = m.config.Config.get('site', 'site_domain', None) from_settings = get_setting_value('SERVER_NAME', None) if from_settings and not from_settings.startswith('http'): from_settings = 'http://%s/' % from_settings From 0e5a416c9f33ba9353e816c64aaf49e2e25843e8 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 4 Dec 2015 20:35:08 -0200 Subject: [PATCH 09/65] importing new style for some extensions --- quokka/core/admin/models.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/quokka/core/admin/models.py b/quokka/core/admin/models.py index 6627ae859..ecfb4208e 100644 --- a/quokka/core/admin/models.py +++ b/quokka/core/admin/models.py @@ -3,17 +3,17 @@ import random import datetime -from flask.ext.admin.contrib.mongoengine import ModelView -from flask.ext.admin.contrib.fileadmin import FileAdmin as _FileAdmin -from flask.ext.admin.babel import gettext, ngettext -from flask.ext.admin import AdminIndexView -from flask.ext.admin import BaseView as AdminBaseView -from flask.ext.admin.actions import action -from flask.ext.admin import helpers as h -from flask.ext.security import current_user -from flask.ext.security.utils import url_for_security from flask import redirect, flash, url_for, Response, current_app +from flask_admin.contrib.mongoengine import ModelView +from flask_admin.contrib.fileadmin import FileAdmin as _FileAdmin +from flask_admin.babel import gettext, ngettext +from flask_admin import AdminIndexView +from flask_admin import BaseView as AdminBaseView +from flask_admin.actions import action +from flask_admin import helpers as h +from flask_security import current_user +from flask_security.utils import url_for_security from flask.ext.htmlbuilder import html from quokka.modules.accounts.models import User From 496b8aac4b852d317c1ca473f858a19fa29c576e Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 4 Dec 2015 20:35:42 -0200 Subject: [PATCH 10/65] quokka endpoints should now have a namespace quokka.core or .module --- quokka/core/app.py | 15 ++++++++++++++- quokka/core/models/content.py | 2 +- quokka/core/models/subcontent.py | 2 +- quokka/core/views.py | 2 +- quokka/ext/oauthlib.py | 4 ++-- quokka/ext/views.py | 26 +++++++++++++------------- quokka/ext/weasyprint.py | 5 +++-- quokka/modules/accounts/views.py | 16 ++++++++++++++-- 8 files changed, 49 insertions(+), 23 deletions(-) diff --git a/quokka/core/app.py b/quokka/core/app.py index 2fb9b8477..428bd4df7 100644 --- a/quokka/core/app.py +++ b/quokka/core/app.py @@ -1,4 +1,5 @@ from flask import Flask, Blueprint +from flask.helpers import _endpoint_from_view_func from quokka.core.config import QuokkaConfig from quokka.utils.aliases import dispatch_aliases @@ -23,6 +24,18 @@ def preprocess_request(self): return dispatch_aliases() or super(QuokkaApp, self).preprocess_request() + def add_quokka_url_rule(self, rule, endpoint=None, + view_func=None, **options): + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) + if not endpoint.startswith('quokka.'): + endpoint = 'quokka.core.' + endpoint + self.add_url_rule(rule, endpoint, view_func, **options) + class QuokkaModule(Blueprint): - "for future overriding" + "Overwrite blueprint namespace to quokka.modules.name" + + def __init__(self, name, *args, **kwargs): + name = "quokka.modules." + name + super(QuokkaModule, self).__init__(name, *args, **kwargs) diff --git a/quokka/core/models/content.py b/quokka/core/models/content.py index f6972f15d..2d987ccd3 100644 --- a/quokka/core/models/content.py +++ b/quokka/core/models/content.py @@ -86,7 +86,7 @@ def get_main_image_url(self, thumb=False, path = self.contents.get(identifier=item).content.path else: path = self.contents.get(identifier=item).content.thumb - return url_for('media', filename=path) + return url_for('quokka.core.media', filename=path) except Exception as e: logger.warning('get_main_image_url:' + str(e)) diff --git a/quokka/core/models/subcontent.py b/quokka/core/models/subcontent.py index 2203f50a7..98cc21aba 100644 --- a/quokka/core/models/subcontent.py +++ b/quokka/core/models/subcontent.py @@ -37,7 +37,7 @@ class SubContent(Publishable, Ordered, db.EmbeddedDocument): @property def thumb(self): try: - return url_for('media', filename=self.content.thumb) + return url_for('quokka.core.media', filename=self.content.thumb) # return self.content.thumb except Exception as e: logger.warning(str(e)) diff --git a/quokka/core/views.py b/quokka/core/views.py index 5118982e4..b86a4904a 100644 --- a/quokka/core/views.py +++ b/quokka/core/views.py @@ -243,7 +243,7 @@ def get_context(self, long_slug, render_content=False): len(long_slug.split('/')) < 3 and \ not render_content: slug = long_slug.split('/')[-1] - return redirect(url_for('detail', long_slug=slug)) + return redirect(url_for('quokka.core.detail', long_slug=slug)) filters = self.get_filters() diff --git a/quokka/ext/oauthlib.py b/quokka/ext/oauthlib.py index 25675a0b3..bb464c4fb 100644 --- a/quokka/ext/oauthlib.py +++ b/quokka/ext/oauthlib.py @@ -42,7 +42,7 @@ def configure(app): } """ - app.add_url_rule( + app.add_quokka_url_rule( '/accounts/oauth/login//', 'oauth_login', oauth_login @@ -66,7 +66,7 @@ def configure(app): setattr(app, provider_name, oauth_app) - app.add_url_rule( + app.add_quokka_url_rule( '/accounts/oauth/authorized/{0}/'.format(provider), '{0}_authorized'.format(provider), oauth_app.authorized_handler(make_oauth_handler(provider)) diff --git a/quokka/ext/views.py b/quokka/ext/views.py index 7bbe3b1be..cf894d6e8 100644 --- a/quokka/ext/views.py +++ b/quokka/ext/views.py @@ -40,47 +40,47 @@ def static_from_root(): def configure(app): - app.add_url_rule('/mediafiles/', view_func=media) - app.add_url_rule('/template_files/', + app.add_quokka_url_rule('/mediafiles/', view_func=media) + app.add_quokka_url_rule('/template_files/', view_func=template_files) - app.add_url_rule('/theme_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_url_rule(filepath, view_func=static_from_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_url_rule('/.{0}'.format(content_extension), + app.add_quokka_url_rule('/.{0}'.format(content_extension), view_func=ContentDetail.as_view('detail')) # Draft preview - app.add_url_rule('/.preview', + app.add_quokka_url_rule('/.preview', view_func=ContentDetailPreview.as_view('preview')) # Atom Feed - app.add_url_rule( + app.add_quokka_url_rule( '/.atom', view_func=FeedAtom.as_view('atom_list') ) - app.add_url_rule( + app.add_quokka_url_rule( '/tag/.atom', view_func=TagAtom.as_view('atom_tag') ) # RSS Feed - app.add_url_rule( + app.add_quokka_url_rule( '/.xml', view_func=FeedRss.as_view('rss_list') ) - app.add_url_rule('/tag/.xml', view_func=TagRss.as_view('rss_tag')) + app.add_quokka_url_rule('/tag/.xml', view_func=TagRss.as_view('rss_tag')) # Tag list - app.add_url_rule('/tag//', view_func=TagList.as_view('tag')) + app.add_quokka_url_rule('/tag//', view_func=TagList.as_view('tag')) # Match channels by its long_slug mpath - app.add_url_rule('//', + app.add_quokka_url_rule('//', view_func=ContentList.as_view('list')) # Home page - app.add_url_rule( + app.add_quokka_url_rule( '/', view_func=ContentList.as_view('home'), defaults={"long_slug": Channel.get_homepage('long_slug') or "home"} diff --git a/quokka/ext/weasyprint.py b/quokka/ext/weasyprint.py index 66d0333de..cb0092758 100644 --- a/quokka/ext/weasyprint.py +++ b/quokka/ext/weasyprint.py @@ -31,6 +31,7 @@ def configure(app): if app.config.get('ENABLE_TO_PDF', False) and not import_error: def render_to_pdf(long_slug): - return render_pdf(url_for('detail', long_slug=long_slug)) + return render_pdf(url_for('quokka.core.detail', long_slug=long_slug)) - app.add_url_rule('/.pdf', view_func=render_to_pdf) + app.add_quokka_url_rule('/.pdf', + view_func=render_to_pdf) diff --git a/quokka/modules/accounts/views.py b/quokka/modules/accounts/views.py index 477793ef9..2cb4c2e87 100644 --- a/quokka/modules/accounts/views.py +++ b/quokka/modules/accounts/views.py @@ -58,7 +58,10 @@ def needs_login(self, **kwargs): if not current_user.is_authenticated(): nex = kwargs.get( 'next', - request.values.get('next', url_for('accounts.profile_edit')) + request.values.get( + 'next', + url_for('quokka.modules.accounts.profile_edit') + ) ) return redirect(url_for_security('login', next=nex)) @@ -69,4 +72,13 @@ def get(self): ) def post(self): - return redirect(url_for('accounts.profile_edit')) + form = self.form(request.form) + if form.validate(): + user = get_current_user() + form.populate_obj(user) + user.save() + flash('Profile saved!', 'alert') + return redirect(url_for('quokka.modules.accounts.profile_edit')) + else: + flash('Error ocurred!', 'alert error') # form errors + return render_template('accounts/profile_edit.html', form=form) From 9060386567c280b45744857ea5e7ad87213ba90a Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 4 Dec 2015 20:36:11 -0200 Subject: [PATCH 11/65] flashes messages and user avatar page --- quokka/modules/accounts/models.py | 24 ++++++++++++++++++++++- quokka/modules/accounts/views.py | 4 +++- quokka/themes/pure/static/css/custom.css | 12 ++++++++++++ quokka/themes/pure/templates/sidebar.html | 2 ++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/quokka/modules/accounts/models.py b/quokka/modules/accounts/models.py index ddb74957c..02402f389 100644 --- a/quokka/modules/accounts/models.py +++ b/quokka/modules/accounts/models.py @@ -63,7 +63,12 @@ class User(db.DynamicDocument, ThemeChanger, HasCustomValue, UserMixin): links = db.ListField(db.EmbeddedDocumentField(UserLink)) use_avatar_from = db.StringField( - choices=(("gravatar", "gravatar"), ("url", "url"), ("file", "file")), + choices=( + ("gravatar", "gravatar"), + ("url", "url"), + ("file", "file"), + ("facebook", "facebook") + ), default='gravatar' ) gravatar_email = db.EmailField(max_length=255) @@ -71,6 +76,23 @@ class User(db.DynamicDocument, ThemeChanger, HasCustomValue, UserMixin): avatar_url = db.StringField(max_length=255) # facebook image should be get from connections + def get_avatar(self, *args, **kwargs): + if self.use_avatar_from == 'url': + return self.avatar_url + elif self.use_avatar_from == 'file': + return self.avatar_file_path + elif self.use_avatar_from == 'facebook': + try: + return Connection.objects( + provider_id='facebook', + user_id=self.id, + ).first().image_url + except Exception as e: + flash('Error: %s' % str(e)) + return Gravatar()( + self.get_gravatar_email(), *args, **kwargs + ) + @property def summary(self): return (self.bio or self.tagline or '')[:255] diff --git a/quokka/modules/accounts/views.py b/quokka/modules/accounts/views.py index 2cb4c2e87..791abf9fc 100644 --- a/quokka/modules/accounts/views.py +++ b/quokka/modules/accounts/views.py @@ -20,7 +20,9 @@ class SwatchView(MethodView): """ def post(self): - current_user.set_swatch(request.form.get('swatch')) + swatch = request.form.get('swatch') + current_user.set_swatch(swatch) + flash('Theme successfully changed to %s' % swatch, 'alert') return redirect(url_for('admin.index')) diff --git a/quokka/themes/pure/static/css/custom.css b/quokka/themes/pure/static/css/custom.css index 0a81c4f4e..86c7207e6 100644 --- a/quokka/themes/pure/static/css/custom.css +++ b/quokka/themes/pure/static/css/custom.css @@ -276,3 +276,15 @@ nav.pure-menu .pure-menu-list .pure-menu-link { .post-header h1 { color: #005DA4; } + +.flashes li { + background-color: green; + color: white; + font-weight: bold; + padding: 5px; +} + +.flashes .error{ + background-color: red; + font-weight: bold; +} diff --git a/quokka/themes/pure/templates/sidebar.html b/quokka/themes/pure/templates/sidebar.html index b23c09ade..0cfb99e56 100644 --- a/quokka/themes/pure/templates/sidebar.html +++ b/quokka/themes/pure/templates/sidebar.html @@ -7,6 +7,8 @@ {% endif %} > + {% include theme('flashes.html') %} +
From 008e30e459fe3d44fd67d49ec8a6b4d5e4aca631 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 4 Dec 2015 20:42:42 -0200 Subject: [PATCH 12/65] added a reminder --- quokka/ext/blueprints.py | 1 + 1 file changed, 1 insertion(+) diff --git a/quokka/ext/blueprints.py b/quokka/ext/blueprints.py index b95651213..b630d7b55 100644 --- a/quokka/ext/blueprints.py +++ b/quokka/ext/blueprints.py @@ -34,6 +34,7 @@ def load_from_folder(app): 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, '__init__.py')): + # TODO: CHANGE TO main.py # register blueprint object module_name = ".".join([base_module_name, fname]) From 40c22defa7c4a0cace9e148591d74212eb179174 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sat, 5 Dec 2015 01:16:40 -0200 Subject: [PATCH 13/65] endpoints in url_for updated using quokka. namespace --- quokka/core/admin/__init__.py | 4 +- quokka/core/admin/models.py | 5 ++- quokka/core/models/content.py | 2 +- quokka/ext/views.py | 17 ++++--- quokka/ext/weasyprint.py | 3 +- quokka/modules/media/admin.py | 2 +- quokka/modules/posts/models.py | 2 +- quokka/settings.py | 4 +- .../themes/admin/templates/admin/index.html | 44 +++++++++---------- quokka/themes/pure/templates/_helpers.html | 18 ++++---- quokka/themes/pure/templates/_menu.html | 2 +- .../pure/templates/accounts/profile_edit.html | 2 +- .../themes/pure/templates/authors/detail.html | 4 +- .../themes/pure/templates/authors/list.html | 4 +- quokka/themes/pure/templates/base.html | 4 +- .../pure/templates/content/comments.html | 2 +- .../pure/templates/content/tag_list.html | 2 +- 17 files changed, 66 insertions(+), 55 deletions(-) diff --git a/quokka/core/admin/__init__.py b/quokka/core/admin/__init__.py index cfa85005b..a23ab5dd6 100644 --- a/quokka/core/admin/__init__.py +++ b/quokka/core/admin/__init__.py @@ -31,11 +31,13 @@ class QuokkaAdmin(Admin): registered = [] + def register(self, model, view=None, *args, **kwargs): _view = view or ModelAdmin admin_view_exclude = get_setting_value('ADMIN_VIEW_EXCLUDE', []) identifier = '.'.join((model.__module__, model.__name__)) - if identifier not in admin_view_exclude and not identifier in self.registered: + if (identifier not in admin_view_exclude) and ( + identifier not in self.registered): self.add_view(_view(model, *args, **kwargs)) self.registered.append(identifier) diff --git a/quokka/core/admin/models.py b/quokka/core/admin/models.py index ecfb4208e..58de2507e 100644 --- a/quokka/core/admin/models.py +++ b/quokka/core/admin/models.py @@ -64,7 +64,10 @@ def format_datetime(self, request, obj, fieldname, *args, **kwargs): def view_on_site(self, request, obj, fieldname, *args, **kwargs): available = obj.is_available - endpoint = kwargs.pop('endpoint', 'detail' if available else 'preview') + endpoint = kwargs.pop( + 'endpoint', + 'quokka.core.detail' if available else 'quokka.core.preview' + ) return html.a( href=obj.get_absolute_url(endpoint), target='_blank', diff --git a/quokka/core/models/content.py b/quokka/core/models/content.py index 2d987ccd3..ddaad46cb 100644 --- a/quokka/core/models/content.py +++ b/quokka/core/models/content.py @@ -108,7 +108,7 @@ def get_http_url(self): absolute_url = absolute_url[1:] return u"{}{}".format(site_url, absolute_url) - def get_absolute_url(self, endpoint='detail'): + def get_absolute_url(self, endpoint='quokka.core.detail'): if self.channel.is_homepage: long_slug = self.slug else: diff --git a/quokka/ext/views.py b/quokka/ext/views.py index cf894d6e8..2ebf4638e 100644 --- a/quokka/ext/views.py +++ b/quokka/ext/views.py @@ -42,9 +42,11 @@ def static_from_root(): def configure(app): 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) + 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) @@ -52,11 +54,11 @@ def configure(app): # 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')) + view_func=ContentDetail.as_view('detail')) # Draft preview app.add_quokka_url_rule('/.preview', - view_func=ContentDetailPreview.as_view('preview')) + view_func=ContentDetailPreview.as_view('preview')) # Atom Feed app.add_quokka_url_rule( @@ -71,14 +73,15 @@ def configure(app): 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')) + 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')) + view_func=ContentList.as_view('list')) # Home page app.add_quokka_url_rule( '/', diff --git a/quokka/ext/weasyprint.py b/quokka/ext/weasyprint.py index cb0092758..b53e3c921 100644 --- a/quokka/ext/weasyprint.py +++ b/quokka/ext/weasyprint.py @@ -31,7 +31,8 @@ def configure(app): if app.config.get('ENABLE_TO_PDF', False) and not import_error: def render_to_pdf(long_slug): - return render_pdf(url_for('quokka.core.detail', long_slug=long_slug)) + return render_pdf(url_for('quokka.core.detail', + long_slug=long_slug)) app.add_quokka_url_rule('/.pdf', view_func=render_to_pdf) diff --git a/quokka/modules/media/admin.py b/quokka/modules/media/admin.py index a171f8b3c..40ec1f148 100644 --- a/quokka/modules/media/admin.py +++ b/quokka/modules/media/admin.py @@ -116,7 +116,7 @@ def _list_thumbnail(self, context, model, name): base_path=lazy_media_path(), thumbnail_size=get_setting_value('MEDIA_IMAGE_THUMB_SIZE', default=(200, 200, True)), - endpoint="media", + endpoint="quokka.core.media", namegen=dated_path, permission=0o777, allowed_extensions="MEDIA_IMAGE_ALLOWED_EXTENSIONS", diff --git a/quokka/modules/posts/models.py b/quokka/modules/posts/models.py index 58c4d2a9c..62811a445 100644 --- a/quokka/modules/posts/models.py +++ b/quokka/modules/posts/models.py @@ -6,5 +6,5 @@ class Post(Content): - # URL_NAMESPACE = 'posts.detail' + # URL_NAMESPACE = 'quokka.modules.posts.detail' body = db.StringField(required=True) diff --git a/quokka/settings.py b/quokka/settings.py index 5ff89199c..a657a0f7b 100644 --- a/quokka/settings.py +++ b/quokka/settings.py @@ -70,6 +70,8 @@ just develop or download and drop in your modules folder by default it is in /modules, you can change if needed """ + +BLUEPRINTS_MODULE_NAME = 'main' BLUEPRINTS_PATH = 'modules' BLUEPRINTS_OBJECT_NAME = 'module' @@ -275,7 +277,7 @@ SENTRY_ENABLED = False SENTRY_DSN = "" -# html or markdown +# html or markdown or plaintext DEFAULT_TEXT_FORMAT = "html" "Shortner urls configuration" diff --git a/quokka/themes/admin/templates/admin/index.html b/quokka/themes/admin/templates/admin/index.html index b79a8baa3..1debc22aa 100644 --- a/quokka/themes/admin/templates/admin/index.html +++ b/quokka/themes/admin/templates/admin/index.html @@ -4,7 +4,7 @@
-
+
{%if config.get('ADMIN_HEADER') %}{{config.get('ADMIN_HEADER')|safe}}{% endif %}

{{ admin_view.admin.name }}

{{_gettext('Latest Posts')}}:

@@ -15,7 +15,7 @@

{{_gettext('Latest Posts')}}:

| {{_gettext('Edit')}} - + {{_gettext('View') if post.published else _gettext('Preview')}} {% else %} @@ -24,25 +24,25 @@

{{_gettext('Latest Posts')}}:

-
-

{{_gettext('Latest Comments')}}:

-
    - {% for comment in get_comments(limit=3) %} -
  • - {{ comment.author_name }} {{ _gettext('says') }}: -
    {{ comment.body[:140] }} -
    - - - - -
  • - {% else %} - {{ _gettext("No comments yet!") }} - {{ _gettext("Internal comment system is enabled, if you want to change it click in config button and change from 'internal' to 'disqus' in comments configuration and set your disqus_script.") }} - {% endfor %} -
-
+{#
#} +{#

{{_gettext('Latest Comments')}}:

#} +{#
    #} +{# {% for comment in get_comments(limit=3) %}#} +{#
  • #} +{# {{ comment.author_name }} {{ _gettext('says') }}:#} +{#
    {{ comment.body[:140] }}#} +{#
    #} +{# #} +{# #} +{# #} +{# #} +{#
  • #} +{# {% else %}#} +{# {{ _gettext("No comments yet!") }}#} +{# {{ _gettext("Internal comment system is enabled, if you want to change it click in config button and change from 'internal' to 'disqus' in comments configuration and set your disqus_script.") }}#} +{# {% endfor %}#} +{#
#} +{#
#}
@@ -60,7 +60,7 @@

{{_gettext('Latest Comments')}}:

-
+ + + + {% for field in form %} {% if field.type in ['CSRFTokenField', 'HiddenField'] %} diff --git a/quokka/themes/pure/templates/authors/detail.html b/quokka/themes/pure/templates/authors/detail.html index 347de3244..c425e1bc1 100644 --- a/quokka/themes/pure/templates/authors/detail.html +++ b/quokka/themes/pure/templates/authors/detail.html @@ -15,7 +15,7 @@ - + {% endblock meta %} {% block content %} diff --git a/quokka/themes/pure/templates/authors/list.html b/quokka/themes/pure/templates/authors/list.html index 5d0fbb3b3..41746595b 100644 --- a/quokka/themes/pure/templates/authors/list.html +++ b/quokka/themes/pure/templates/authors/list.html @@ -19,7 +19,7 @@

All authors

{% if author.email %}
- {{ author.display_name }} + {{ author.display_name }}
{{ author.display_name }}
From 9c36820e153c2b60b57db04e1d5aef4b6c6d007b Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sun, 6 Dec 2015 14:26:57 -0200 Subject: [PATCH 19/65] channel can redirect to endpoint --- quokka/core/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/quokka/core/views.py b/quokka/core/views.py index 059f405f1..9602bcae1 100644 --- a/quokka/core/views.py +++ b/quokka/core/views.py @@ -77,7 +77,11 @@ def get(self, long_slug): published_channels = Channel.objects(published=True).values_list('id') if channel.redirect_url: - return redirect(channel.redirect_url) + url_protos = ('http://', 'mailto:', '/', 'ftp://') + if channel.redirect_url.startswith(url_protos): + return redirect(channel.redirect_url) + else: + return redirect(url_for(channel.redirect_url)) if channel.render_content: return ContentDetail().get( From 284fe054a7b4f51adfadc9c2956e9832dc6c6495 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sun, 6 Dec 2015 14:27:07 -0200 Subject: [PATCH 20/65] added unicode to userlink --- quokka/modules/accounts/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quokka/modules/accounts/models.py b/quokka/modules/accounts/models.py index d096c08f4..183d09421 100644 --- a/quokka/modules/accounts/models.py +++ b/quokka/modules/accounts/models.py @@ -39,6 +39,9 @@ class UserLink(db.EmbeddedDocument): css_class = db.StringField(max_length=50) order = db.IntField(default=0) + def __unicode__(self): + return u"{0} - {1}".format(self.title, self.link) + class User(db.DynamicDocument, ThemeChanger, HasCustomValue, UserMixin): name = db.StringField(max_length=255) From 5027e5f5d8b06e9db7e7c2a88055fefe4a088e07 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sun, 6 Dec 2015 14:27:23 -0200 Subject: [PATCH 21/65] fix generate_username to avoid duplications --- quokka/modules/accounts/models.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/quokka/modules/accounts/models.py b/quokka/modules/accounts/models.py index 183d09421..7b487785c 100644 --- a/quokka/modules/accounts/models.py +++ b/quokka/modules/accounts/models.py @@ -72,7 +72,7 @@ class User(db.DynamicDocument, ThemeChanger, HasCustomValue, UserMixin): choices=( ("gravatar", "gravatar"), ("url", "url"), - ("file", "file"), + ("upload", "upload"), ("facebook", "facebook") ), default='gravatar' @@ -84,7 +84,7 @@ class User(db.DynamicDocument, ThemeChanger, HasCustomValue, UserMixin): def get_avatar_url(self, *args, **kwargs): if self.use_avatar_from == 'url': return self.avatar_url - elif self.use_avatar_from == 'file': + elif self.use_avatar_from == 'upload': return url_for( 'quokka.core.media', filename=self.avatar_file_path ) @@ -117,10 +117,13 @@ def clean(self, *args, **kwargs): super(User, self).clean(*args, **kwargs) @classmethod - def generate_username(cls, name): + def generate_username(cls, name, user=None): name = name or '' username = slugify(name) - if cls.objects.filter(username=username).count(): + filters = {"username": username} + if user: + filters["id__ne"] = user.id + if cls.objects.filter(**filters).count(): username = "{0}{1}".format(username, randint(1, 1000)) return username From d4b478b6ef5707b794149dbfb555b630ae947fdc Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sun, 6 Dec 2015 14:27:47 -0200 Subject: [PATCH 22/65] user edit profile page is complete --- quokka/modules/accounts/views.py | 46 ++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/quokka/modules/accounts/views.py b/quokka/modules/accounts/views.py index 9eb1f6f32..0bf458cf8 100644 --- a/quokka/modules/accounts/views.py +++ b/quokka/modules/accounts/views.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- import os from werkzeug import secure_filename -from flask import redirect, request, url_for, flash +from flask import redirect, request, url_for, flash, current_app from flask.views import MethodView from quokka.utils import get_current_user from quokka.utils.upload import lazy_media_path @@ -48,10 +48,9 @@ class ProfileEditView(MethodView): 'tagline', 'bio', 'use_avatar_from', - 'avatar_file_path', 'gravatar_email', 'avatar_url', - 'links', + # 'links', # fix multifields ] ) @@ -68,9 +67,14 @@ def needs_login(**kwargs): return redirect(url_for_security('login', next=nex)) def get(self): + user = get_current_user() + context = {} + for link in user.links: + context[link.icon] = link.link return self.needs_login() or render_template( 'accounts/profile_edit.html', - form=self.form(instance=get_current_user()) + form=self.form(instance=user), + **context ) def post(self): @@ -90,9 +94,41 @@ def post(self): avatar.save(path) form.populate_obj(user) user.avatar_file_path = avatar_file_path + if avatar: + user.use_avatar_from = 'upload' + user.username = User.generate_username( + user.username or user.name, user=user + ) + + self.update_user_links(request.form, user) + user.save() flash('Profile saved!', 'alert') - return redirect(url_for('quokka.modules.accounts.profile_edit')) + return redirect( + request.args.get('next') or + url_for('quokka.modules.accounts.profile_edit') + ) else: flash('Error ocurred!', 'alert error') # form errors return render_template('accounts/profile_edit.html', form=form) + + @staticmethod + def update_user_links(form, user): + for item in ['facebook', 'twitter', 'github', 'globe']: + data = form.get(item) + try: + if data: + user.links.update( + {'link': data}, icon=item + ) or user.links.create( + icon=item, + css_class=item, + title=item, + link=data + ) + else: + user.links.delete(icon=item) + except Exception as e: + current_app.logger.error( + 'Error updating user links: %s' % e + ) From a2a4f01fdc59463163c90806e875f3f92196779a Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sun, 6 Dec 2015 14:27:56 -0200 Subject: [PATCH 23/65] authors ordered by name or AUTHORS_ORDER --- quokka/modules/authors/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quokka/modules/authors/utils.py b/quokka/modules/authors/utils.py index 09ea9ada6..760b1d6ec 100644 --- a/quokka/modules/authors/utils.py +++ b/quokka/modules/authors/utils.py @@ -38,7 +38,7 @@ def get_authors(*args, **kwargs): name__in=['author'], **kwargs ) - ) + ).order_by(current_app.config.get("AUTHORS_ORDER", "name")) disabled_pagination = False if not current_app.config.get("AUTHORS_PAGINATION_ENABLED", True): From 9632089c4069462b6c173569c1d42e00a9b3ff5e Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sun, 6 Dec 2015 14:28:10 -0200 Subject: [PATCH 24/65] url to profile edit added to admin menu --- quokka/themes/admin/templates/admin/base.html | 1 + 1 file changed, 1 insertion(+) diff --git a/quokka/themes/admin/templates/admin/base.html b/quokka/themes/admin/templates/admin/base.html index 1dce65079..df31109e9 100644 --- a/quokka/themes/admin/templates/admin/base.html +++ b/quokka/themes/admin/templates/admin/base.html @@ -64,6 +64,7 @@ {% if security.changeable %}
  • Change password
  • {% endif %} +
  • Edit Profile
  • Logout
  • From 394a0107e572ab1ee2744184a959e9bca8cff9ef Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sun, 6 Dec 2015 14:28:27 -0200 Subject: [PATCH 25/65] user edit form html --- quokka/themes/pure/static/css/custom.css | 4 ++ .../pure/templates/accounts/profile_edit.html | 46 ++++++++++++++----- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/quokka/themes/pure/static/css/custom.css b/quokka/themes/pure/static/css/custom.css index 86c7207e6..cf19eddb5 100644 --- a/quokka/themes/pure/static/css/custom.css +++ b/quokka/themes/pure/static/css/custom.css @@ -227,6 +227,10 @@ nav.pure-menu { border-radius: 120px; } + +.small_author_avatar { + border-radius: 60px; +} /* menu home page */ nav.pure-menu .pure-menu-children { background-color: #ddd; diff --git a/quokka/themes/pure/templates/accounts/profile_edit.html b/quokka/themes/pure/templates/accounts/profile_edit.html index b4e18cbd0..1c51986be 100644 --- a/quokka/themes/pure/templates/accounts/profile_edit.html +++ b/quokka/themes/pure/templates/accounts/profile_edit.html @@ -9,26 +9,50 @@

    Edit Profile

    - +
    + Personal data: +
    + + + +
    - - - - {% for field in form %} - + {% for field in form %} {% if field.type in ['CSRFTokenField', 'HiddenField'] %} {{ field() }} - {%elif field.type == 'FieldList' %} + {% elif field.type == 'FieldList' %} {{field()}} {% else %} {{ render_field_with_errors(field, class="form-control") }} {% endif %} - {% endfor %} +
    +
    + Personal links: +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + +
    @@ -36,4 +60,4 @@

    Edit Profile

    -{% endblock %} +{% endblock %} \ No newline at end of file From a799d0f4cbdaebd6a8f322a6b2fed69ee78090c9 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sun, 6 Dec 2015 14:28:32 -0200 Subject: [PATCH 26/65] avoid None in template --- quokka/themes/pure/templates/authors/list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quokka/themes/pure/templates/authors/list.html b/quokka/themes/pure/templates/authors/list.html index 41746595b..f7f561eb2 100644 --- a/quokka/themes/pure/templates/authors/list.html +++ b/quokka/themes/pure/templates/authors/list.html @@ -24,7 +24,7 @@

    All authors

    {{ author.display_name }}
    - {{author.get_value('position')}} + {{author.get_value('position') or ''}} From 090005db671348f77f4489893dcce5b2388776ae Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sun, 6 Dec 2015 14:58:37 -0200 Subject: [PATCH 27/65] added fields to usre admin --- quokka/modules/accounts/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quokka/modules/accounts/admin.py b/quokka/modules/accounts/admin.py index 4635989a8..13903e7d2 100644 --- a/quokka/modules/accounts/admin.py +++ b/quokka/modules/accounts/admin.py @@ -17,7 +17,9 @@ class UserAdmin(ModelAdmin): 'confirmed_at', 'last_login_at', 'current_login_at', 'last_login_ip', 'current_login_ip', 'login_count', 'tagline', - 'gravatar_email', 'bio', 'links', 'values') + 'gravatar_email', 'use_avatar_from', + 'avatar_file_path', 'avatar_url', + 'bio', 'links', 'values') form_extra_fields = { "newpassword": TextField(widget=PasswordInput()) From 6e90c9c5820eb0193be5c41b1335fbb6b3bcb9b6 Mon Sep 17 00:00:00 2001 From: Thiago Avelino Date: Fri, 11 Dec 2015 23:10:57 -0200 Subject: [PATCH 28/65] removed elasticsearch docker, not using We use version control, do not need to keep commented code --- docker-compose.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 57d527996..0ed09d9ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,15 +5,6 @@ mongo: volumes: - ./etc/mongodata:/data/db -# elastic: -# image: catholabs/elastic-with-marvel -# command: elasticsearch -# volumes: -# - etc/elasticdata:/usr/share/elasticsearch/data -# ports: -# - "9200:9200" -# mem_limit: 1000000000 - web: restart: always image: quokka/quokkadev @@ -24,7 +15,6 @@ web: - .:/quokka links: - mongo:mongo - # - elastic:elastic command: sh etc/docker_wait_to_start.sh environment: - QUOKKA_MONGODB_HOST=mongo From 94eae13ddd82d2d1f73033f2c887ec0d441e56d8 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Tue, 15 Dec 2015 08:53:52 -0200 Subject: [PATCH 29/65] bump requirements versions --- requirements/requirements.txt | 3 ++- requirements/test.txt | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 986f46f4d..31d21a86c 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -11,7 +11,8 @@ Flask-Gravatar==0.4.2 Flask-Mistune==0.1.1 quokka-flask-mongoengine==0.7.4 Flask-OAuthlib==0.9.2 -flask-security==1.7.4 +# Flask-Login broke compatibility +flask-security==1.7.4 # rq.filter: <1.7.5 Pillow==3.0.0 PyRSS2Gen==1.1 requests==2.8.1 diff --git a/requirements/test.txt b/requirements/test.txt index be5097f36..60478feb1 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,13 +1,13 @@ # testing coveralls mock==1.3.0 -pytest==2.8.3 +pytest==2.8.5 pytest-cov==2.2.0 pytest-flask==0.10.0 Flask-Testing==0.4.2 # style check -flake8==2.5.0 +flake8==2.5.1 pep8-naming==0.3.3 flake8-debugger==1.4.0 flake8-print==2.0.1 From 170a274dbd585e90c8a4722ce397da3ec2696712 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Tue, 15 Dec 2015 09:53:49 -0200 Subject: [PATCH 30/65] removed sitemap from static, added comments to settings --- quokka/settings.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/quokka/settings.py b/quokka/settings.py index 68694cd61..b5a55b859 100644 --- a/quokka/settings.py +++ b/quokka/settings.py @@ -40,10 +40,13 @@ """ Not needed by flask, but those root folders are used -by FLask-Admin file manager +by FLask-Admin file manager and Media module """ PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) + +# If you need different folder to save media files MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'mediafiles') + STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static') ROOT_DIR = os.path.abspath(os.path.join(PROJECT_ROOT, '..')) @@ -56,7 +59,7 @@ Files on MAP_STATIC_ROOT will be served from /static/ example: /static/favicon.ico will be served by site.com/favicon.ico """ -MAP_STATIC_ROOT = ('/robots.txt', '/sitemap.xml', '/favicon.ico') +MAP_STATIC_ROOT = ('/robots.txt', '/favicon.ico') """ @@ -71,8 +74,8 @@ by default it is in /modules, you can change if needed """ -BLUEPRINTS_MODULE_NAME = 'main' BLUEPRINTS_PATH = 'modules' +BLUEPRINTS_MODULE_NAME = 'main' BLUEPRINTS_OBJECT_NAME = 'module' """ @@ -239,6 +242,13 @@ MEDIA_VIDEO_ALLOWED_EXTENSIONS = ('avi', 'mp4', 'mpeg') MEDIA_FILE_ALLOWED_EXTENSIONS = ('pdf', 'txt', 'doc', 'docx', 'xls', 'xmlsx') +""" +Quokka-Themes checks `THEME_PATHS` configuration variable to find +directories that contain themes. The theme's identifier in info.json +must match the name of its directory. +""" +# THEME_PATHS = '/etc/themes/' + # default admin THEME ADMIN_THEME = 'admin' """ From 8d57f950159c4a9ca6cc16bc4c4058822e608bdf Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Tue, 15 Dec 2015 12:29:12 -0200 Subject: [PATCH 31/65] added support to @opbeat awesome error tracking and metrics --- etc/openshift_settings.py | 10 +++++++++- quokka/ext/__init__.py | 16 ++-------------- quokka/ext/development.py | 28 ++++++++++++++++++++++++++++ quokka/ext/sentry.py | 6 ------ quokka/settings.py | 25 +++++++++++++++++++++++++ requirements/dev.txt | 1 + 6 files changed, 65 insertions(+), 21 deletions(-) create mode 100644 quokka/ext/development.py delete mode 100644 quokka/ext/sentry.py diff --git a/etc/openshift_settings.py b/etc/openshift_settings.py index 030617eaf..809c396eb 100644 --- a/etc/openshift_settings.py +++ b/etc/openshift_settings.py @@ -24,6 +24,7 @@ LOGGER_FORMAT = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s' LOGGER_DATE_FORMAT = '%d.%m %H:%M:%S' +DEBUG = False if os.environ['OPENSHIFT_APP_NAME'] == 'quokkadevelopment': DEBUG_TOOLBAR_ENABLED = True DEBUG = True @@ -33,7 +34,6 @@ MAP_STATIC_ROOT = ( '/robots.txt', - '/sitemap.xml', '/favicon.ico', '/vaddy-c603c78bbeba8d9.html' ) @@ -51,3 +51,11 @@ "" "" ) + +OPBEAT = { + 'ORGANIZATION_ID': '87883697e9f64e85b1a8170ff664890a', + 'APP_ID': 'd924873719', + 'SECRET_TOKEN': 'a0fee12cdb9eca36320b42d2d50f57ceea260dc5', + 'INCLUDE_PATHS': ['quokka'], + 'DEBUG': DEBUG, +} diff --git a/quokka/ext/__init__.py b/quokka/ext/__init__.py index 4d89841df..e2954bd80 100644 --- a/quokka/ext/__init__.py +++ b/quokka/ext/__init__.py @@ -7,7 +7,7 @@ from . import (generic, babel, blueprints, error_handlers, context_processors, template_filters, before_request, views, themes, fixtures, - oauthlib, weasyprint, security) + oauthlib, weasyprint, security, development) def configure_extensions(app, admin): @@ -26,22 +26,10 @@ def configure_extensions(app, admin): blueprints.load_from_folder(app) weasyprint.configure(app) configure_admin(app, admin) - - if app.config.get('DEBUG_TOOLBAR_ENABLED'): - try: - from flask_debugtoolbar import DebugToolbarExtension - DebugToolbarExtension(app) - except: - app.logger.info('flask_debugtoolbar is not installed') - + development.configure(app, admin) before_request.configure(app) views.configure(app) oauthlib.configure(app) - - if app.config.get('SENTRY_ENABLED', False): - from .sentry import configure - configure(app) - return app diff --git a/quokka/ext/development.py b/quokka/ext/development.py new file mode 100644 index 000000000..70c6cc9e2 --- /dev/null +++ b/quokka/ext/development.py @@ -0,0 +1,28 @@ +# coding: utf-8 + + +def configure(app, admin=None): + if app.config.get('DEBUG_TOOLBAR_ENABLED'): + try: + from flask_debugtoolbar import DebugToolbarExtension + DebugToolbarExtension(app) + except ImportError: + app.logger.info('flask_debugtoolbar is not installed') + + if app.config.get('OPBEAT'): + try: + from opbeat.contrib.flask import Opbeat + Opbeat( + app, + logging=app.config.get('OPBEAT', {}).get('LOGGING', False) + ) + app.logger.info('opbeat configured!!!') + except ImportError: + app.logger.info('opbeat is not installed') + + if app.config.get('SENTRY_ENABLED', False): + try: + from raven.contrib.flask import Sentry + app.sentry = Sentry(app) + except ImportError: + app.logger.info('sentry, raven is not installed') diff --git a/quokka/ext/sentry.py b/quokka/ext/sentry.py deleted file mode 100644 index 2542e49dc..000000000 --- a/quokka/ext/sentry.py +++ /dev/null @@ -1,6 +0,0 @@ -# coding: utf-8 - - -def configure(app): - from raven.contrib.flask import Sentry - app.sentry = Sentry(app) diff --git a/quokka/settings.py b/quokka/settings.py index b5a55b859..ab3c52ae4 100644 --- a/quokka/settings.py +++ b/quokka/settings.py @@ -326,3 +326,28 @@ HTTP method (such as PATCH). """ HTTP_PROXY_METHOD_OVERRIDE = False + + +""" +https://opbeat.com is application monitoring tool +you can enable it but you need to install requirements/dev.txt +https://opbeat.com/docs/articles/get-started-with-flask/ + +OPBEAT = { + 'ORGANIZATION_ID': '', + 'APP_ID': '', + 'SECRET_TOKEN': '', + 'INCLUDE_PATHS': ['quokka'], + 'DEBUG': True, + 'LOGGING': False +} + +Notify Opbeat when a release has completed +$ curl https://intake.opbeat.com/api/v1/ + organizations//apps//releases/ \ + -H "Authorization: Bearer " \ + -d rev=`git log -n 1 --pretty=format:%H` \ + -d branch=`git rev-parse --abbrev-ref HEAD` \ + -d status=completed +""" +# OPBEAT = None diff --git a/requirements/dev.txt b/requirements/dev.txt index 83d64091b..70e9d6b58 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,3 +1,4 @@ Flask-DebugToolbar==0.10.0 ipython==4.0.1 ipdb==0.8.1 +opbeat==3.1.3 From 9bd75573c4e4e64459ff44c3d6e4fdad4161373a Mon Sep 17 00:00:00 2001 From: Avelino Date: Wed, 16 Dec 2015 08:47:41 -0200 Subject: [PATCH 32/65] basic sitemap (initial bootstrap) ref #324 --- quokka/core/views.py | 41 +++++++++++++++++++++++++++++++++++++++++ quokka/ext/views.py | 4 +++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/quokka/core/views.py b/quokka/core/views.py index 9602bcae1..45d5c4e26 100644 --- a/quokka/core/views.py +++ b/quokka/core/views.py @@ -555,3 +555,44 @@ def get(self, tag): ) return self.make_rss(feed_name, contents) + + +class SiteMap(MethodView): + @staticmethod + def make_external_url(url): + return urljoin(request.url_root, url) + + def get_contents(self): + now = datetime.now() + filters = { + 'published': True, + 'available_at__lte': now, + } + + contents = Content.objects().filter(**filters) + return contents + + def sitemap_render(self, contents): + tmpl = """ + + + + """ + for content in contents: + tmpl += """ + + {0} + {1} + daily + 0.2 + + """.format( + self.make_external_url(content.get_absolute_url()), + content.created_at + ) + tmpl += "" + return tmpl + + def get(self): + contents = self.get_contents() + return self.sitemap_render(contents) diff --git a/quokka/ext/views.py b/quokka/ext/views.py index 2ebf4638e..301186644 100644 --- a/quokka/ext/views.py +++ b/quokka/ext/views.py @@ -9,7 +9,7 @@ ContentList, TagList ) -from quokka.core.views import TagAtom, FeedAtom, TagRss, FeedRss +from quokka.core.views import TagAtom, FeedAtom, TagRss, FeedRss, SiteMap from quokka.core.models.channel import Channel @@ -40,6 +40,8 @@ def static_from_root(): def configure(app): + 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) From 6efc54f5385d82208076a1b8b73235040d626040 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Wed, 16 Dec 2015 22:38:40 -0200 Subject: [PATCH 33/65] Update requests version --- Makefile | 12 ++---------- requirements/requirements.txt | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index c6cc40fb4..cbf209d00 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,20 @@ -.PHONY: run +.PHONY: run shell test install pep8 clean + run: python manage.py runserver --reloader --debug -.PHONY: shell shell: python manage.py shell -.PHONY: test test: pep8 QUOKKA_MODE=test py.test --cov=quokka -l --tb=short --maxfail=1 quokka/ -.PHONY: install install: python setup.py develop -.PHONY: pep8 pep8: @flake8 quokka --ignore=F403 --exclude=migrations -.PHONY: sdist -sdist: test - @python setup.py sdist upload - -.PHONY: clean clean: @find ./ -name '*.pyc' -exec rm -f {} \; @find ./ -name 'Thumbs.db' -exec rm -f {} \; diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 31d21a86c..978880407 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -15,7 +15,7 @@ Flask-OAuthlib==0.9.2 flask-security==1.7.4 # rq.filter: <1.7.5 Pillow==3.0.0 PyRSS2Gen==1.1 -requests==2.8.1 +requests==2.9.0 quokka-speaklater==1.3.1 pyshorteners==0.5.8 cached-property==1.3.0 From e82340b6b056ad0664d4ab62a0464bd9ae0d7b4b Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Wed, 16 Dec 2015 23:38:58 -0200 Subject: [PATCH 34/65] Added sitemap by template, fix get_http_url for channel --- quokka/core/models/channel.py | 10 +++--- quokka/core/views.py | 46 ++++++++++-------------- quokka/ext/__init__.py | 3 +- quokka/themes/pure/templates/sitemap.xml | 18 ++++++++++ 4 files changed, 43 insertions(+), 34 deletions(-) create mode 100644 quokka/themes/pure/templates/sitemap.xml diff --git a/quokka/core/models/channel.py b/quokka/core/models/channel.py index 61449c645..2cca21e32 100644 --- a/quokka/core/models/channel.py +++ b/quokka/core/models/channel.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- import logging -from flask import request from flask.ext.mistune import markdown from quokka.core.db import db @@ -10,9 +9,8 @@ from quokka.core.models.signature import ( Tagged, Publishable, LongSlugged, ContentFormat, TemplateType ) -from quokka.core.models.config import Config from quokka.core.admin.utils import _l - +from quokka.utils.settings import get_site_url logger = logging.getLogger() @@ -161,8 +159,10 @@ def get_canonical_url(self, *args, **kwargs): return self.get_absolute_url() def get_http_url(self): - site_url = Config.get('site', 'site_domain', request.url_root) - return u"{0}{1}".format(site_url, self.get_absolute_url()) + site_url = get_site_url() + absolute_url = self.get_absolute_url() + absolute_url = absolute_url[1:] + return u"{0}{1}".format(site_url, absolute_url) def clean(self): homepage = Channel.objects(is_homepage=True) diff --git a/quokka/core/views.py b/quokka/core/views.py index 45d5c4e26..364a8a82e 100644 --- a/quokka/core/views.py +++ b/quokka/core/views.py @@ -1,10 +1,10 @@ # coding: utf-8 +import sys import logging -import collections import hashlib +import collections import PyRSS2Gen as pyrss -import sys from datetime import datetime, timedelta from flask import request, redirect, url_for, abort, current_app @@ -19,10 +19,8 @@ # python3 support if sys.version_info.major == 3: from urllib.parse import urljoin - # from io import StringIO else: from urlparse import urljoin - # import StringIO logger = logging.getLogger() @@ -568,31 +566,25 @@ def get_contents(self): 'published': True, 'available_at__lte': now, } - contents = Content.objects().filter(**filters) return contents - def sitemap_render(self, contents): - tmpl = """ - - - - """ - for content in contents: - tmpl += """ - - {0} - {1} - daily - 0.2 - - """.format( - self.make_external_url(content.get_absolute_url()), - content.created_at - ) - tmpl += "" - return tmpl + def get_channels(self): + now = datetime.now() + filters = { + 'published': True, + 'available_at__lte': now, + } + channels = Channel.objects().filter(**filters) + return channels def get(self): - contents = self.get_contents() - return self.sitemap_render(contents) + """ + Fixme: Should include extra paths, fixed paths + config based paths, static paths + """ + return render_template( + 'sitemap.xml', + contents=self.get_contents(), + channels=self.get_channels() + ) diff --git a/quokka/ext/__init__.py b/quokka/ext/__init__.py index 4d89841df..23c443034 100644 --- a/quokka/ext/__init__.py +++ b/quokka/ext/__init__.py @@ -1,6 +1,5 @@ # coding: utf-8 -from flask.ext.mail import Mail - +from flask_mail import Mail from quokka.core.db import db from quokka.core.cache import cache from quokka.core.admin import configure_admin diff --git a/quokka/themes/pure/templates/sitemap.xml b/quokka/themes/pure/templates/sitemap.xml new file mode 100644 index 000000000..557749ef3 --- /dev/null +++ b/quokka/themes/pure/templates/sitemap.xml @@ -0,0 +1,18 @@ +{% macro render_item(item) %} + + {{item.get_http_url()|safe}} + {{item.available_at.strftime("%Y-%m-%d %H:%M:%S")}} + daily + 0.2 + +{% endmacro %} + + + + {% for item in channels %} + {{ render_item(item) }} + {% endfor %} + {% for item in contents %} + {{ render_item(item) }} + {% endfor %} + \ No newline at end of file From 0edae311c81d883120f5122813ef652a2af6e340 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 17 Dec 2015 00:03:05 -0200 Subject: [PATCH 35/65] appropriate escaping on sitemap.xml --- quokka/themes/pure/templates/sitemap.xml | 27 ++++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/quokka/themes/pure/templates/sitemap.xml b/quokka/themes/pure/templates/sitemap.xml index 557749ef3..b1e2f3aca 100644 --- a/quokka/themes/pure/templates/sitemap.xml +++ b/quokka/themes/pure/templates/sitemap.xml @@ -1,18 +1,17 @@ -{% macro render_item(item) %} - - {{item.get_http_url()|safe}} - {{item.available_at.strftime("%Y-%m-%d %H:%M:%S")}} - daily - 0.2 - -{% endmacro %} - +{%- macro render_item(item) -%} + + {{item.get_http_url()|safe}} + {{item.available_at.strftime("%Y-%m-%d %H:%M:%S")}} + daily + 0.2 + +{%- endmacro -%} - {% for item in channels %} + {%- for item in channels -%} {{ render_item(item) }} - {% endfor %} - {% for item in contents %} + {%- endfor -%} + {%- for item in contents -%} {{ render_item(item) }} - {% endfor %} - \ No newline at end of file + {%- endfor -%} + From f01cb0fbf10c7e65978a1b3c7770ce7aeb156872 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 17 Dec 2015 00:10:45 -0200 Subject: [PATCH 36/65] using isoformat for sitemap --- quokka/themes/pure/templates/sitemap.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quokka/themes/pure/templates/sitemap.xml b/quokka/themes/pure/templates/sitemap.xml index b1e2f3aca..19e59eb3d 100644 --- a/quokka/themes/pure/templates/sitemap.xml +++ b/quokka/themes/pure/templates/sitemap.xml @@ -1,7 +1,7 @@ {%- macro render_item(item) -%} {{item.get_http_url()|safe}} - {{item.available_at.strftime("%Y-%m-%d %H:%M:%S")}} + {{item.available_at.isoformat()}} daily 0.2 From 59651462b34bd795de2802bc0b3a74b93795b46f Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 17 Dec 2015 08:26:20 -0200 Subject: [PATCH 37/65] missing package added to alpine linux image --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 992f7c9fd..8ce36cfa7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ COPY requirements/requirements.txt /tmp/ COPY requirements/test.txt /tmp/ COPY requirements/dev.txt /tmp/ RUN apk update -RUN apk add gcc python py-pip libjpeg zlib zlib-dev tiff freetype git py-pillow python-dev musl-dev bash +RUN apk add gcc python py-pip jpeg libjpeg jpeg-dev zlib zlib-dev tiff freetype git py-pillow python-dev musl-dev bash RUN pip install -r /tmp/requirements.txt RUN pip install -r /tmp/test.txt RUN pip install -r /tmp/dev.txt From c23809513e55b1d79a572d7a3979ed9165b2907a Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 17 Dec 2015 08:41:47 -0200 Subject: [PATCH 38/65] pin Pillow to previous version, because 3.0.0 broken in some O.S --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 978880407..d835699df 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -13,7 +13,7 @@ quokka-flask-mongoengine==0.7.4 Flask-OAuthlib==0.9.2 # Flask-Login broke compatibility flask-security==1.7.4 # rq.filter: <1.7.5 -Pillow==3.0.0 +Pillow==2.9.0 # rq.filter:<3.0.0 PyRSS2Gen==1.1 requests==2.9.0 quokka-speaklater==1.3.1 From 2c379bf4a5ba5cd3f6773dbac3d2adcbe55439ce Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 17 Dec 2015 08:42:58 -0200 Subject: [PATCH 39/65] added comment explaining why pillow is outdated --- requirements/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index d835699df..d48a4185b 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -13,6 +13,7 @@ quokka-flask-mongoengine==0.7.4 Flask-OAuthlib==0.9.2 # Flask-Login broke compatibility flask-security==1.7.4 # rq.filter: <1.7.5 +# Pillow 3.0.0 raises --enable-zlib and --enable-jpeg error on some O.S Pillow==2.9.0 # rq.filter:<3.0.0 PyRSS2Gen==1.1 requests==2.9.0 From bad1de32c671f35da005aa92293921b4a13d61e9 Mon Sep 17 00:00:00 2001 From: Thiago Avelino Date: Thu, 17 Dec 2015 09:02:40 -0200 Subject: [PATCH 40/65] fixed format date on sitemap google webmasters erro: "An invalid date was found. Please fix the date or formatting before resubmitting." --- quokka/themes/pure/templates/sitemap.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quokka/themes/pure/templates/sitemap.xml b/quokka/themes/pure/templates/sitemap.xml index 19e59eb3d..6f4c3b9af 100644 --- a/quokka/themes/pure/templates/sitemap.xml +++ b/quokka/themes/pure/templates/sitemap.xml @@ -1,7 +1,7 @@ {%- macro render_item(item) -%} {{item.get_http_url()|safe}} - {{item.available_at.isoformat()}} + {{item.available_at.strftime('%Y-%m-%d')}} daily 0.2 From d6d9e6778b2b86511c9a122b2d7dbc04d7a86e10 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 17 Dec 2015 19:52:49 -0200 Subject: [PATCH 41/65] more configs to landscape --- .landscape.yaml | 15 ++++++++++++++- quokka/core/config.py | 24 ------------------------ 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/.landscape.yaml b/.landscape.yaml index a6de678b3..f5160039e 100644 --- a/.landscape.yaml +++ b/.landscape.yaml @@ -1,3 +1,11 @@ +python-targets: + - 2 + - 3 + +uses: + - celery + - flask + pylint: disable: - bare-except @@ -28,4 +36,9 @@ requirements: - requirements/requirements.txt - requirements/test.txt -#max-line-length: 120 + +# doc-warnings: yes +# test-warnings: no +# strictness: veryhigh +# max-line-length: 120 +# autodetect: yes diff --git a/quokka/core/config.py b/quokka/core/config.py index b018dff14..a3a6f0571 100644 --- a/quokka/core/config.py +++ b/quokka/core/config.py @@ -46,30 +46,6 @@ def get_from_db(self, key, default=None): def __getitem__(self, key): return self.get_from_db(key) or dict.__getitem__(self, key) - # def __iter__(self): - # return iter(self.store) - - # def __len__(self): - # return len(self.store) - - # def __repr__(self): - # return self.store.__repr__() - - # def __unicode__(self): - # return unicode(repr(self.store)) - - # def __call__(self, *args, **kwargs): - # return self.store.get(*args, **kwargs) - - # def __contains__(self, item): - # return item in self.store - - # def keys(self): - # return self.store.keys() - - # def values(self): - # return self.store.values() - def get(self, key, default=None): return self.get_from_db(key) or self.store.get(key) or default From fe5357eded9e8805d239bbfaf4261a09fa0dfe31 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 17 Dec 2015 19:53:12 -0200 Subject: [PATCH 42/65] config cast should use @ instead of $ --- quokka/utils/__init__.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/quokka/utils/__init__.py b/quokka/utils/__init__.py index f2a5f8526..e17dd53f3 100644 --- a/quokka/utils/__init__.py +++ b/quokka/utils/__init__.py @@ -53,23 +53,23 @@ def is_accessible(roles_accepted=None, user=None): def parse_conf_data(data): """ - $int $bool $float $json (for lists and dicts) + @int @bool @float @json (for lists and dicts) strings does not need converters export QUOKKA_DEFAULT_THEME='material' - export QUOKKA_DEBUG='$bool True' - export QUOKKA_DEBUG_TOOLBAR_ENABLED='$bool False' - export QUOKKA_PAGINATION_PER_PAGE='$int 20' - export QUOKKA_MONGODB_SETTINGS='$json {"DB": "quokka_db", "HOST": "mongo"}' - export QUOKKA_ALLOWED_EXTENSIONS='$json ["jpg", "png"]' + export QUOKKA_DEBUG='@bool True' + export QUOKKA_DEBUG_TOOLBAR_ENABLED='@bool False' + export QUOKKA_PAGINATION_PER_PAGE='@int 20' + export QUOKKA_MONGODB_SETTINGS='@json {"DB": "quokka_db", "HOST": "mongo"}' + export QUOKKA_ALLOWED_EXTENSIONS='@json ["jpg", "png"]' """ import json - true_values = ('t', 'true', 'enabled', '1', 'on') + true_values = ('t', 'true', 'enabled', '1', 'on', 'yes') converters = { - '$int': int, - '$float': float, - '$bool': lambda value: True if value.lower() in true_values else False, - '$json': json.loads + '@int': int, + '@float': float, + '@bool': lambda value: True if value.lower() in true_values else False, + '@json': json.loads } if data.startswith(tuple(converters.keys())): parts = data.partition(' ') From 1499b90e5ba9f203dd5f8a74d8b20d5170d1effe Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 17 Dec 2015 20:22:09 -0200 Subject: [PATCH 43/65] Update .landscape.yaml --- .landscape.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.landscape.yaml b/.landscape.yaml index f5160039e..3d5daa24f 100644 --- a/.landscape.yaml +++ b/.landscape.yaml @@ -3,7 +3,6 @@ python-targets: - 3 uses: - - celery - flask pylint: From 10972e0959f5f772f021e0f5ced34febf0281632 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 18 Dec 2015 14:34:30 -0200 Subject: [PATCH 44/65] Update .landscape.yaml --- .landscape.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.landscape.yaml b/.landscape.yaml index 3d5daa24f..97643e656 100644 --- a/.landscape.yaml +++ b/.landscape.yaml @@ -1,9 +1,9 @@ -python-targets: - - 2 - - 3 +# python-targets: +# - 2 +# - 3 -uses: - - flask +# uses: +# - flask pylint: disable: From aec69f34d38842adf5e5c150c37c3816b91b57e7 Mon Sep 17 00:00:00 2001 From: Avelino Date: Fri, 18 Dec 2015 15:12:23 -0200 Subject: [PATCH 45/65] ignore all *.log files --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 504e7eee9..7c9efb30d 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,4 @@ etc/mongodata/*.0 etc/mongodata/*.ns etc/mongodata/*.lock etc/mongodata/*.bson -etc/logs/*.log \ No newline at end of file +*.log \ No newline at end of file From 0a74b23fdd2f2f43bb2e4fd0d58d53754752e529 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 18 Dec 2015 19:53:40 -0200 Subject: [PATCH 46/65] Update README.md --- README.md | 84 ++++++++++--------------------------------------------- 1 file changed, 14 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 44a78a9c6..9cc21c21a 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,13 @@ User 'admin@example.com' and passwd 'admin' to login in to /admin # Get Quokka -## Get Quokka Master and enter in to its root directory + +# Using Docker + +The easiest way to run Quokka for development or production is using quokkaCMS + Gunicorn + Supervisor under a Docker Container. You can see the instructions in the following repository: https://github.com/quokkaproject/docker-gunicorn-supervisor + + +## Get Quokka to run locally for development or deployment ```bash git clone https://github.com/quokkaproject/quokka --branch master --single-branch @@ -58,11 +64,7 @@ cd quokka > if you are cloning to contribute to the project just clone it without the "--branch=master --single-branch" part -## Run Quokka - -You have 2 options **RUN LOCAL** or **RUN IN DOCKER** - -### RUN LOCAL +### Run Quokka Install needed packages in your local computer @@ -94,6 +96,8 @@ You can install everything you need in your local computer or if preferred use a MONGODB_USERNAME = None MONGODB_PASSWORD = None ============================================================= + + # You can also use envvars `export QUOKKA_MONGO_DB="yourdbname"` ``` 3. If you have Docker installed you can simply run the official Mongo image @@ -117,7 +121,8 @@ You can install everything you need in your local computer or if preferred use a #### Python requirements Install all needed python packages -> activate your virtualenv if you want + +> If you have a virtualenv, activate it! `source env/bin/activate` or `workon env` ```bash pip install -r requirements/requirements.txt @@ -135,7 +140,7 @@ pip install -r requirements/requirements.txt P4$$W0Rd ``` - 4. Populate with sample data (optional if you want sample data) + 4. Populate with sample data (optional if you want sample data for testing) ```bash $ python manage.py populate @@ -148,68 +153,7 @@ pip install -r requirements/requirements.txt ``` - Site on [http://localhost:5000](http://localhost:5000) - Admin on [http://localhost:5000/admin](http://localhost:5000/admin) - - -### RUN IN DOCKER - -- Run pre built docker images with everything pre-installed -- You will need docker and docker compose installed in your machine -- Once in Docker all data is stored behind quokka/etc folder - - -#### Install Docker and docker-compose - -- **Docker** - https://docs.docker.com/installation/ -- **Docker-Compose** - https://docs.docker.com/compose/install/ - - -> Ensure that local port 27017(mongo) is not being used on your computer - -* ### Run with docker-compose - - 1. Easiest way is just running the following command in quokka root folder - ```bash - docker-compose up - ``` - - > use -d on above to leave it as a daemon - - 2. You can create a new admin user to login and start posting - ```bash - docker-compose run web python manage.py accounts_createsuperuser - ``` - - 3. Or populate with sample data (optional) - ```bash - docker-compose run web python manage.py populate - ``` - > credentials for /admin will be email: admin@example.com passwd: admin - - 4. You enter Quokka Shell (in a separate console) or run any other command in place of **shell* - ```bash - docker-compose run web python manage.py shell - ``` - -* ### Run standalone containers -> (each in separate shells or use -d option) - - 1. run mongo container - ```bash - docker run -v $PWD/etc/mongodata:/data/db -p 27017:27017 --name mongo mongo - ``` - - 2. run quokka web app container - ```bash - docker run -e "QUOKKA_MONGODB_HOST=mongo" -p 5000:5000 --link mongo:mongo -v $PWD:/quokka --workdir /quokka -t -i quokka/quokkadev python manage.py runserver --host 0.0.0.0 - - ``` - - 3. run quokka shell if needed - ```bash - docker run -e "QUOKKA_MONGODB_HOST=mongo" -p 5000:5000 --link mongo:mongo -v $PWD:/quokka --workdir /quokka -t -i quokka/quokkadev python manage.py shell - - ``` - + ## Deployment From 63462ce9d6b58665e4de87c4a1eac5559dfaadba Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sat, 19 Dec 2015 10:42:37 -0200 Subject: [PATCH 47/65] added "populate_reset" command to clean sample data --- manage.py | 13 ++++++++ quokka/ext/fixtures.py | 33 +++++++++++------- quokka/utils/populate.py | 72 ++++++++++++++++++++++++++++------------ 3 files changed, 85 insertions(+), 33 deletions(-) diff --git a/manage.py b/manage.py index c676b8055..28d66b8eb 100755 --- a/manage.py +++ b/manage.py @@ -72,6 +72,19 @@ def populate(filename, baseurl=None): Populate(db, filepath=filename, baseurl=baseurl, app=app)() +@core_cmd.command() +@click.option( + '-f', + '--filename', + help='Fixtures JSON path', + default='./etc/fixtures/initial_data.json') +@click.option('-b', '--baseurl', help='base url to use', default=None) +def populate_reset(filename, baseurl=None): + """De-Populate the database with sample data""" + from quokka.utils.populate import Populate + Populate(db, filepath=filename, baseurl=baseurl, app=app).reset() + + @core_cmd.command() def showconfig(): """Print all config variables""" diff --git a/quokka/ext/fixtures.py b/quokka/ext/fixtures.py index 24ebb1fd0..af3fe246a 100644 --- a/quokka/ext/fixtures.py +++ b/quokka/ext/fixtures.py @@ -12,17 +12,26 @@ def configure(app, db): if not is_installed: app.logger.info("Loading fixtures") - populate = Populate(db, filepath=app.config.get('POPULATE_FILEPATH')) - populate.create_configs() - populate.create_purposes() - populate.create_channel_types() - populate.create_base_channels() - populate.role("admin") - populate.role("author") + populate = Populate( + db, + filepath=app.config.get('POPULATE_FILEPATH'), + first_install=True + ) try: - with app.app_context(): - user_data, user_obj = populate.create_initial_superuser() - populate.create_initial_post(user_data, user_obj) + populate.create_configs() + populate.create_purposes() + populate.create_channel_types() + populate.create_base_channels() + populate.role("admin") + populate.role("author") + try: + with app.app_context(): + user_data, user_obj = populate.create_initial_superuser() + populate.create_initial_post(user_data, user_obj) + except Exception as e: + app.logger.warning("Cant create initial user and post: %s" % e) except Exception as e: - app.logger.warning("Cant create initial user and post: %s" % e) - Quokka.objects.create(slug="is_installed") + app.logger.error("Error loading fixtures: %s" % e) + populate.reset() + else: + Quokka.objects.create(slug="is_installed") diff --git a/quokka/utils/populate.py b/quokka/utils/populate.py index de01fa06e..63e512e6a 100644 --- a/quokka/utils/populate.py +++ b/quokka/utils/populate.py @@ -5,7 +5,7 @@ from quokka.core.models.subcontent import SubContentPurpose from quokka.core.models.channel import Channel, ChannelType -from quokka.core.models.config import Config +from quokka.core.models.config import Config, Quokka from quokka.core.models.content import License from quokka.core.models.custom_values import CustomValue from quokka.modules.accounts.models import User, Role @@ -233,32 +233,32 @@ def create_purposes(self): self.create_purpose(purpose) def create_initial_post(self, user_data=None, user_obj=None): - post_data = { - "title": "Try Quokka CMS! write a post.", - "summary": ( + post_data = dict( + title="Try Quokka CMS! write a post.", + summary=( "Use default credentials to access " "/admin \r\n" "user: {user[email]} \r\n" "pass: {user[password]} \r\n" ).format(user=user_data), - "slug": "try-quokka-cms", - "tags": ["quokka"], - "body": ( - "## You can try Quokka ADMIN\r\n\r\n" - "Create some posts\r\n\r\n" - "> Use default credentials to access " - "[/admin](/admin) \r\n\r\n" - "- user: {user[email]}\r\n" - "- password: {user[password]}\r\n" - "> ATTENTION! Copy the credentials and delete this post" + slug="try-quokka-cms", + tags=["quokka"], + body=( + "## You can try Quokka ADMIN\r\n\r\n" + "Create some posts\r\n\r\n" + "> Use default credentials to access " + "[/admin](/admin) \r\n\r\n" + "- user: {user[email]}\r\n" + "- password: {user[password]}\r\n" + "> ATTENTION! Copy the credentials and delete this post" ).format(user=user_data), - "license": { - "title": "Creative Commons", - "link": "http://creativecommons.com", - "identifier": "creative_commons_by_nc_nd" - }, - "content_format": "markdown" - } + license=dict( + title="Creative Commons", + link="http://creativecommons.com", + identifier="creative_commons_by_nc_nd" + ), + content_format="markdown" + ) post_data['channel'] = self.channels.get("home") post_data["created_by"] = user_obj or self.users.get('author') post = self.create_post(post_data) @@ -301,3 +301,33 @@ def create_posts(self): except: self.create_channels() self.create_post(data) + + def reset(self): + Post.objects( + slug__in=[item['slug'] for item in self.json_data.get('posts')] + ).delete() + + SubContentPurpose.objects( + identifier__in=[ + item['identifier'] for item in self.json_data.get('purposes') + ] + ).delete() + + base_channels = Channel.objects(parent=None) + second_level = Channel.objects(parent__in=base_channels) + third_level = Channel.objects(parent__in=second_level) + + third_level.delete() + second_level.delete() + base_channels.delete() + + Config.objects( + group__in=[item['group'] for item in self.json_data.get('configs')] + ).delete() + + User.objects( + email__in=[item['email'] for item in self.json_data.get('users')] + ).delete() + + if self.kwargs.get('first_install'): + Quokka.objects.delete() From 186f006c6a19626013fd35e2026981df84feffd9 Mon Sep 17 00:00:00 2001 From: Avelino Date: Sat, 19 Dec 2015 20:02:41 -0200 Subject: [PATCH 48/65] added *.pid on ignore git file --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7c9efb30d..995c8e4df 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,5 @@ etc/mongodata/*.0 etc/mongodata/*.ns etc/mongodata/*.lock etc/mongodata/*.bson -*.log \ No newline at end of file +*.log +*.pid \ No newline at end of file From e57034c0adc496ec90c42d9215e7a8a6ed90a3ea Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sat, 19 Dec 2015 21:42:29 -0200 Subject: [PATCH 49/65] fix populate reset --- quokka/ext/fixtures.py | 4 +++- quokka/utils/populate.py | 19 ++++++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/quokka/ext/fixtures.py b/quokka/ext/fixtures.py index af3fe246a..81d70790a 100644 --- a/quokka/ext/fixtures.py +++ b/quokka/ext/fixtures.py @@ -1,6 +1,7 @@ # coding: utf-8 from quokka.utils.populate import Populate from quokka.core.models.config import Quokka +from quokka.core.models.config import Config def configure(app, db): @@ -31,7 +32,8 @@ def configure(app, db): except Exception as e: app.logger.warning("Cant create initial user and post: %s" % e) except Exception as e: - app.logger.error("Error loading fixtures: %s" % e) + app.logger.error("Error loading fixtures, try again - %s" % e) populate.reset() + Config.objects.delete() else: Quokka.objects.create(slug="is_installed") diff --git a/quokka/utils/populate.py b/quokka/utils/populate.py index 63e512e6a..c52b6bf2e 100644 --- a/quokka/utils/populate.py +++ b/quokka/utils/populate.py @@ -313,17 +313,14 @@ def reset(self): ] ).delete() - base_channels = Channel.objects(parent=None) - second_level = Channel.objects(parent__in=base_channels) - third_level = Channel.objects(parent__in=second_level) - - third_level.delete() - second_level.delete() - base_channels.delete() - - Config.objects( - group__in=[item['group'] for item in self.json_data.get('configs')] - ).delete() + for channel in Channel.objects( + slug__in=[ + item['slug'] for item in self.json_data.get('channels')]): + for subchannel in channel.get_children(): + for subsubchannel in subchannel.get_children(): + subsubchannel.delete() + subchannel.delete() + channel.delete() User.objects( email__in=[item['email'] for item in self.json_data.get('users')] From 91dd43e396574d57d50abef8657744528c63997f Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Mon, 21 Dec 2015 01:22:12 -0200 Subject: [PATCH 50/65] Fix #328 added get_main_image_http --- quokka/core/models/content.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/quokka/core/models/content.py b/quokka/core/models/content.py index 83401bdee..d34353fb7 100644 --- a/quokka/core/models/content.py +++ b/quokka/core/models/content.py @@ -75,7 +75,7 @@ def available_objects(cls, **filters): def get_main_image_url(self, thumb=False, default=None, identifier='mainimage'): - """ + """method returns the path (url) of the main image """ if not isinstance(identifier, (list, tuple)): identifier = [identifier] @@ -92,6 +92,15 @@ def get_main_image_url(self, thumb=False, return default + def get_main_image_http(self, thumb=False, + default=None, identifier='mainimage'): + """method returns the path of the main image with http + """ + site_url = get_site_url() + image_url = self.get_main_image_url( + thumb=thumb, default=default, identifier=identifier) + return u"{}{}".format(site_url, image_url) + def get_uid(self): return str(self.id) From ef4daa054dbdef6c0b9277e1eb43ad8e84f04953 Mon Sep 17 00:00:00 2001 From: Avelino Date: Mon, 21 Dec 2015 15:21:03 -0200 Subject: [PATCH 51/65] used get_main_image_http on `og:image` --- quokka/themes/pure/templates/content/default_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quokka/themes/pure/templates/content/default_detail.html b/quokka/themes/pure/templates/content/default_detail.html index 14710531a..5f88d7411 100644 --- a/quokka/themes/pure/templates/content/default_detail.html +++ b/quokka/themes/pure/templates/content/default_detail.html @@ -34,7 +34,7 @@ - + {% endblock meta %} {% block content %} From c7a36886132718171026ae154069f361095827ec Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sat, 16 Jan 2016 13:04:26 -0200 Subject: [PATCH 52/65] mistune 0.7.x broke base64 images --- requirements/requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index d48a4185b..8350dab39 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -31,3 +31,6 @@ pymongo==2.9.1 # rq.filter: <3 # There is a compatibility break in Flask-Login 0.3 Flask-Login==0.2.11 # rq.filter: <0.3 + +# Mistune 0.7.1 broke base64; inline images +mistune==0.6 From 6ae76a37e79b7ecd9c8522c2c69e80395f3e68ab Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sun, 17 Jan 2016 12:21:44 -0200 Subject: [PATCH 53/65] No need for versioning and other stuff in __init__ it is a CMS --- quokka/__init__.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/quokka/__init__.py b/quokka/__init__.py index 91545fbce..8476003b0 100644 --- a/quokka/__init__.py +++ b/quokka/__init__.py @@ -1,16 +1,5 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -VERSION = (0, 2, 0) - -__version__ = ".".join(map(str, VERSION)) -__status__ = "Alpha" -__description__ = "Flexible & modular CMS powered by Flask and MongoDB" -__author__ = "Bruno Rocha " -__email__ = "quokka-developers@googlegroups.com" -__license__ = "MIT License" -__copyright__ = "Copyright 2014, Quokka Project" - - from quokka.core.admin import create_admin from quokka.core.app import QuokkaApp from quokka.core.middleware import HTTPMethodOverrideMiddleware From 529f8a462687594394659cadc215d652768f79c0 Mon Sep 17 00:00:00 2001 From: Prayag Verma Date: Tue, 26 Jan 2016 17:58:08 +0530 Subject: [PATCH 54/65] Mention initial copyright year I am not a lawyer but according to http://www.copyright.gov/circs/circ01.pdf (See screenshot of relevant section below) , listing the first year of publication in the copyright is a good thing ![selection_008](https://cloud.githubusercontent.com/assets/829526/12409934/7021c3a6-be95-11e5-8d1a-18f6948571e0.png) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index b8c182c9c..59a741f1a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Licensed under MIT license. http://opensource.org/licenses/MIT -Copyright (c) 2015 Quokka Project. and other contributors +Copyright (c) 2013-2016 Quokka Project. and other contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the From 2f34a9d6b0869db9d551968da8183924a1339b04 Mon Sep 17 00:00:00 2001 From: "requires.io" Date: Sat, 6 Feb 2016 17:30:21 -0200 Subject: [PATCH 55/65] [requires.io] dependency update --- requirements/requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8350dab39..765159377 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,7 +5,7 @@ quokka-flask-htmlbuilder==0.10 awesome-slugify==1.6.5 blinker==1.4 Flask==0.10.1 -Flask-BabelEx==0.9.2 +Flask-BabelEx==0.9.3 Flask-Cache==0.13.1 Flask-Gravatar==0.4.2 Flask-Mistune==0.1.1 @@ -16,7 +16,7 @@ flask-security==1.7.4 # rq.filter: <1.7.5 # Pillow 3.0.0 raises --enable-zlib and --enable-jpeg error on some O.S Pillow==2.9.0 # rq.filter:<3.0.0 PyRSS2Gen==1.1 -requests==2.9.0 +requests==2.9.1 quokka-speaklater==1.3.1 pyshorteners==0.5.8 cached-property==1.3.0 @@ -27,10 +27,10 @@ click==6.2 mongoengine==0.10.1 # rq.filter:<0.10.2 # mongoengine is not working yet with pymongo3 -pymongo==2.9.1 # rq.filter: <3 +pymongo==2.9.2 # rq.filter: <3 # There is a compatibility break in Flask-Login 0.3 Flask-Login==0.2.11 # rq.filter: <0.3 # Mistune 0.7.1 broke base64; inline images -mistune==0.6 +mistune==0.7.1 From 985d9d71cc0314dc7e8167ba3a4ea820fb627dea Mon Sep 17 00:00:00 2001 From: "requires.io" Date: Sat, 6 Feb 2016 17:30:22 -0200 Subject: [PATCH 56/65] [requires.io] dependency update --- requirements/dev.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 70e9d6b58..e93e8a6dd 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,4 +1,4 @@ Flask-DebugToolbar==0.10.0 -ipython==4.0.1 +ipython==4.1.1 ipdb==0.8.1 -opbeat==3.1.3 +opbeat==3.2.1 From 67ff4b07e6c5e7c983b8fc644a1d86c56c2f077c Mon Sep 17 00:00:00 2001 From: "requires.io" Date: Sat, 6 Feb 2016 17:30:23 -0200 Subject: [PATCH 57/65] [requires.io] dependency update --- requirements/test.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/test.txt b/requirements/test.txt index 60478feb1..49738ffe2 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,13 +1,13 @@ # testing coveralls mock==1.3.0 -pytest==2.8.5 -pytest-cov==2.2.0 +pytest==2.8.7 +pytest-cov==2.2.1 pytest-flask==0.10.0 Flask-Testing==0.4.2 # style check -flake8==2.5.1 +flake8==2.5.2 pep8-naming==0.3.3 flake8-debugger==1.4.0 flake8-print==2.0.1 From 0f31af3e26354f531cd64cca5b113f8e074c4af8 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sun, 7 Feb 2016 21:24:36 -0200 Subject: [PATCH 58/65] Update requirements.txt --- requirements/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 765159377..c10e7f3ca 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -30,7 +30,7 @@ mongoengine==0.10.1 # rq.filter:<0.10.2 pymongo==2.9.2 # rq.filter: <3 # There is a compatibility break in Flask-Login 0.3 -Flask-Login==0.2.11 # rq.filter: <0.3 +Flask-Login==0.2.11 # rq.filter: <0.3 # Mistune 0.7.1 broke base64; inline images -mistune==0.7.1 +mistune==0.6 # rq.filter: <0.7 From afe2ad49923b279557a7f5e00b81842fb52bf602 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sun, 7 Feb 2016 22:29:51 -0200 Subject: [PATCH 59/65] Trying Pillow 3.1.1 --- requirements/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index c10e7f3ca..fc4a5dc01 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -14,7 +14,8 @@ Flask-OAuthlib==0.9.2 # Flask-Login broke compatibility flask-security==1.7.4 # rq.filter: <1.7.5 # Pillow 3.0.0 raises --enable-zlib and --enable-jpeg error on some O.S -Pillow==2.9.0 # rq.filter:<3.0.0 +#Pillow==2.9.0 # rq.filter:<3.0.0 +Pillow==3.1.1 PyRSS2Gen==1.1 requests==2.9.1 quokka-speaklater==1.3.1 From 22d83a58b6956ff909660fb097891215c650831d Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 12 Feb 2016 11:56:49 -0200 Subject: [PATCH 60/65] Update test.txt --- requirements/test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test.txt b/requirements/test.txt index 49738ffe2..d61851419 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -7,7 +7,7 @@ pytest-flask==0.10.0 Flask-Testing==0.4.2 # style check -flake8==2.5.2 +flake8==2.5.2 # rq.filter:<2.5.3 pep8-naming==0.3.3 flake8-debugger==1.4.0 flake8-print==2.0.1 From ec8922a09ee56ce4d7adeffe762f8cdb41477fd4 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Wed, 2 Mar 2016 14:54:50 -0300 Subject: [PATCH 61/65] Create .gitattributes --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..8d283d2e8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +quokka/themes/* linguist-vendored From e241790b83a6c8529a665332c0377df3777943fd Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 3 Mar 2016 10:38:12 -0300 Subject: [PATCH 62/65] Update README.md --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9cc21c21a..a827b78ce 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ [![Flask Registered](https://img.shields.io/badge/flask-registered-green.svg?style=flat)](https://github.com/pocoo/metaflask) -[![Travis CI](http://img.shields.io/travis/quokkaproject/quokka.svg)](https://travis-ci.org/quokkaproject/quokka) -[![Coverage Status](http://img.shields.io/coveralls/quokkaproject/quokka.svg)](https://coveralls.io/r/quokkaproject/quokka) -[![Code Health](https://landscape.io/github/quokkaproject/quokka/development/landscape.svg?style=flat)](https://landscape.io/github/quokkaproject/quokka/development) -[![Requirements Status](https://requires.io/github/quokkaproject/quokka/requirements.svg?branch=development)](https://requires.io/github/quokkaproject/quokka/requirements/?branch=development) +[![Travis CI](http://img.shields.io/travis/rochacbruno/quokka.svg)](https://travis-ci.org/rochacbruno/quokka) +[![Coverage Status](http://img.shields.io/coveralls/rochacbruno/quokka.svg)](https://coveralls.io/r/rochacbruno/quokka) +[![Code Health](https://landscape.io/github/rochacbruno/quokka/development/landscape.svg?style=flat)](https://landscape.io/github/rochacbruno/quokka/development) +[![Requirements Status](https://requires.io/github/rochacbruno/quokka/requirements.svg?branch=development)](https://requires.io/github/rochacbruno/quokka/requirements/?branch=development) -[![Stories in Ready](https://badge.waffle.io/quokkaproject/quokka.png?label=ready&title=Ready)](http://waffle.io/quokkaproject/quokka) +[![Stories in Ready](https://badge.waffle.io/rochacbruno/quokka.png?label=ready&title=Ready)](http://waffle.io/rochacbruno/quokka) [![Join Slack Chat](https://img.shields.io/badge/JOIN_SLACK-CHAT-green.svg)](https://quokkaslack.herokuapp.com/) [![Slack](http://quokkaslack.herokuapp.com/badge.svg)](https://quokkaproject.slack.com/messages/) @@ -13,7 +13,7 @@ Donate with Paypal [![wercker status](https://app.wercker.com/status/e9cbc4497ee946083aa19fbd3f756c91/m "wercker status")](https://app.wercker.com/project/bykey/e9cbc4497ee946083aa19fbd3f756c91) -[![Launch on OpenShift](http://launch-shifter.rhcloud.com/button.svg)](https://openshift.redhat.com/app/console/application_type/custom?cartridges%5B%5D=python-2.7&cartridges%5B%5D=mongodb-2.4&initial_git_url=https://github.com/quokkaproject/quokka.git&name=quokka&initial_git_branch=master) +[![Launch on OpenShift](http://launch-shifter.rhcloud.com/button.svg)](https://openshift.redhat.com/app/console/application_type/custom?cartridges%5B%5D=python-2.7&cartridges%5B%5D=mongodb-2.4&initial_git_url=https://github.com/rochacbruno/quokka.git&name=quokka&initial_git_branch=master) Quokka project =============================================== @@ -58,7 +58,7 @@ The easiest way to run Quokka for development or production is using quokkaCMS + ## Get Quokka to run locally for development or deployment ```bash -git clone https://github.com/quokkaproject/quokka --branch master --single-branch +git clone https://github.com/rochacbruno/quokka --branch master --single-branch cd quokka ``` @@ -167,18 +167,18 @@ http://quokkaproject.org/documentation > If you want to help writing the docs please go to https://github.com/quokkaproject/quokkaproject.github.io -Also there is a [Wiki](https://github.com/quokkaproject/quokka/wiki) +Also there is a [Wiki](https://github.com/rochacbruno/quokka/wiki) =============================================== > NOTE: the content from wiki will be moved to /documentation -* [About & Features](https://github.com/quokkaproject/quokka/wiki/about) -* [Installing and running](https://github.com/quokkaproject/quokka/wiki/installation) -* [Requirements](https://github.com/quokkaproject/quokka/wiki/requirements) -* [Extending & Installing modules](https://github.com/quokkaproject/quokka/wiki/plugins) -* [Admin interface](https://github.com/quokkaproject/quokka/wiki/screencast) -* [Project tree](https://github.com/quokkaproject/quokka/wiki/project-tree) -* [Team & Committers](https://github.com/quokkaproject/quokka/graphs/contributors) +* [About & Features](https://github.com/rochacbruno/quokka/wiki/about) +* [Installing and running](https://github.com/rochacbruno/quokka/wiki/installation) +* [Requirements](https://github.com/rochacbruno/quokka/wiki/requirements) +* [Extending & Installing modules](https://github.com/rochacbruno/quokka/wiki/plugins) +* [Admin interface](https://github.com/rochacbruno/quokka/wiki/screencast) +* [Project tree](https://github.com/rochacbruno/quokka/wiki/project-tree) +* [Team & Committers](https://github.com/rochacbruno/quokka/graphs/contributors) Hosting @@ -189,7 +189,7 @@ You can host a Quokka website in any VPS or cloud which supports Python and Flas - PythonAnywhere can run Quokka with Mongo hosted at MongoLab - DigitalOcean is a good option for a VPS - Jelastic Cloud has the easiest Quokka deployment - http://docs.jelastic.com/ru/quokka-cms -- OpenShift [one click deploy](https://openshift.redhat.com/app/console/application_type/custom?cartridges%5B%5D=python-2.7&cartridges%5B%5D=mongodb-2.4&initial_git_url=https://github.com/quokkaproject/quokka.git&name=quokka&initial_git_branch=master) +- OpenShift [one click deploy](https://openshift.redhat.com/app/console/application_type/custom?cartridges%5B%5D=python-2.7&cartridges%5B%5D=mongodb-2.4&initial_git_url=https://github.com/rochacbruno/quokka.git&name=quokka&initial_git_branch=master) ![python](docs/python_powered.png) From 26d4a3066d217aec13935cc652569be60a4de5a0 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 3 Mar 2016 11:00:09 -0300 Subject: [PATCH 63/65] Update dev.txt --- requirements/dev.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index e93e8a6dd..896e7313d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,4 +1,4 @@ Flask-DebugToolbar==0.10.0 -ipython==4.1.1 -ipdb==0.8.1 -opbeat==3.2.1 +ipython==4.1.2 +ipdb==0.9.0 +opbeat==3.2.2 From 3d2765bdfe5fd5413b77a749ea04666be04cbd70 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 3 Mar 2016 11:00:37 -0300 Subject: [PATCH 64/65] Update requirements.txt --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index fc4a5dc01..d2a687366 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -22,7 +22,7 @@ quokka-speaklater==1.3.1 pyshorteners==0.5.8 cached-property==1.3.0 shiftpy==0.1.3 -click==6.2 +click==6.3 # tempfix mongoengine==0.10.1 # rq.filter:<0.10.2 From 05974a527ee95aa181040f060d22e862a36ed53b Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 3 Mar 2016 11:01:22 -0300 Subject: [PATCH 65/65] Update test.txt --- requirements/test.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/test.txt b/requirements/test.txt index d61851419..26ce7001a 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,7 +1,7 @@ # testing coveralls mock==1.3.0 -pytest==2.8.7 +pytest==2.9.0 pytest-cov==2.2.1 pytest-flask==0.10.0 Flask-Testing==0.4.2 @@ -10,6 +10,6 @@ Flask-Testing==0.4.2 flake8==2.5.2 # rq.filter:<2.5.3 pep8-naming==0.3.3 flake8-debugger==1.4.0 -flake8-print==2.0.1 +flake8-print==2.0.2 flake8-todo==0.4 -radon==1.2.2 +radon==1.3.0