Skip to content

Commit

Permalink
At least it works
Browse files Browse the repository at this point in the history
  • Loading branch information
smotornyuk committed Jan 9, 2019
1 parent a11a038 commit 645828f
Show file tree
Hide file tree
Showing 42 changed files with 634 additions and 47 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -31,6 +31,9 @@ fl_notes.txt
# custom style
ckan/public/base/less/custom.less

# webassets
ckan/public/webassets

# nosetest coverage output
.coverage
htmlcov/*
Expand Down
46 changes: 46 additions & 0 deletions ckan/cli/asset.py
@@ -0,0 +1,46 @@
# encoding: utf-8

import logging

import click
from webassets import script
from webassets.exceptions import BundleError

from ckan.lib import webassets_tools
from ckan.cli import error_shout

log = logging.getLogger(__name__)


@click.group(name=u'asset', short_help=u'WebAssets commands')
def asset():
pass


@asset.command(u'build', short_help=u'Builds all bundles.')
def build():
u'''Builds bundles, regardless of whether they are changed or not.'''
script.main(['build'], webassets_tools.env)
click.secho(u'Compile assets: SUCCESS', fg=u'green', bold=True)


@asset.command(u'watch', short_help=u'Watch changes in source files.')
def watch():
u'''Start a daemon which monitors source files, and rebuilds bundles.
This can be useful during development, if building is not
instantaneous, and you are losing valuable time waiting for the
build to finish while trying to access your site.
'''
script.main(['watch'], webassets_tools.env)


@asset.command(u'clean', short_help=u'Clear cache.')
def clean():
u'''Will clear out the cache, which after a while can grow quite large.'''
try:
script.main(['clean'], webassets_tools.env)
except BundleError as e:
return error_shout(e)
click.secho(u'Clear cache: SUCCESS', fg=u'green', bold=True)
5 changes: 4 additions & 1 deletion ckan/cli/cli.py
Expand Up @@ -4,7 +4,9 @@

import click

from ckan.cli import click_config_option, db, load_config, search_index, server
from ckan.cli import (
click_config_option, db, load_config, search_index, server, asset
)
from ckan.config.middleware import make_app

log = logging.getLogger(__name__)
Expand All @@ -28,3 +30,4 @@ def ckan(ctx, config, *args, **kwargs):
ckan.add_command(server.run)
ckan.add_command(db.db)
ckan.add_command(search_index.search_index)
ckan.add_command(asset.asset)
3 changes: 3 additions & 0 deletions ckan/config/environment.py
Expand Up @@ -24,6 +24,7 @@
import ckan.logic as logic
import ckan.authz as authz
import ckan.lib.jinja_extensions as jinja_extensions
from ckan.lib.webassets_tools import webassets_init
from ckan.lib.i18n import build_js_translations

from ckan.common import _, ungettext, config
Expand Down Expand Up @@ -163,6 +164,8 @@ def update_config():
plugin might have changed the config values (for instance it might
change ckan.site_url) '''

webassets_init()

for plugin in p.PluginImplementations(p.IConfigurer):
# must do update in place as this does not work:
# config = plugin.update_config(config)
Expand Down
13 changes: 12 additions & 1 deletion ckan/config/middleware/flask_app.py
Expand Up @@ -7,7 +7,7 @@
import itertools
import pkgutil

from flask import Flask, Blueprint
from flask import Flask, Blueprint, send_from_directory
from flask.ctx import _AppCtxGlobals
from flask.sessions import SessionInterface

Expand All @@ -22,6 +22,7 @@
from repoze.who.config import WhoConfig
from repoze.who.middleware import PluggableAuthenticationMiddleware

import ckan
import ckan.model as model
from ckan.lib import base
from ckan.lib import helpers
Expand Down Expand Up @@ -181,6 +182,16 @@ def hello_world():
def hello_world_post():
return 'Hello World, this was posted to Flask'

# WebAssets
public_folder = config.get('ckan.base_public_folder')
webassets_folder = os.path.join(
os.path.dirname(ckan.__file__), public_folder, 'webassets'
)

@app.route('/webassets/<path:path>')
def webassets(path):
return send_from_directory(webassets_folder, path)

# Auto-register all blueprints defined in the `views` folder
_register_core_blueprints(app)
_register_error_handler(app)
Expand Down
1 change: 1 addition & 0 deletions ckan/lib/extract.py
Expand Up @@ -11,6 +11,7 @@
ckan.lib.jinja_extensions.CkanExtend,
ckan.lib.jinja_extensions.LinkForExtension,
ckan.lib.jinja_extensions.ResourceExtension,
ckan.lib.jinja_extensions.AssetExtension,
ckan.lib.jinja_extensions.UrlForStaticExtension,
ckan.lib.jinja_extensions.UrlForExtension
'''
Expand Down
3 changes: 3 additions & 0 deletions ckan/lib/helpers.py
Expand Up @@ -49,6 +49,7 @@
import ckan

from ckan.common import _, ungettext, c, g, request, session, json
from ckan.lib.webassets_tools import include_asset, render_assets
from markupsafe import Markup, escape


Expand Down Expand Up @@ -2645,6 +2646,8 @@ def clean_html(html):
core_helper(converters.asbool)
# Useful additions from the stdlib.
core_helper(urlencode)
core_helper(include_asset)
core_helper(render_assets)


def load_plugin_helpers():
Expand Down
26 changes: 24 additions & 2 deletions ckan/lib/jinja_extensions.py
Expand Up @@ -34,7 +34,8 @@ def get_jinja_env_options():
LinkForExtension,
ResourceExtension,
UrlForStaticExtension,
UrlForExtension],
UrlForExtension,
AssetExtension],
)


Expand Down Expand Up @@ -312,7 +313,7 @@ def _call(cls, args, kwargs):
return h.nav_link(*args, **kwargs)

class ResourceExtension(BaseExtension):
''' Custom include_resource tag
''' Deprecated. Custom include_resource tag.
{% resource <resource_name> %}
Expand All @@ -323,12 +324,33 @@ class ResourceExtension(BaseExtension):

@classmethod
def _call(cls, args, kwargs):
log.warn(
'`resource` tag is deprecated. '
'Use `assets`'
'<https://docs.ckan.org/en/latest/theming/webassets.html> instead')
assert len(args) == 1
assert len(kwargs) == 0
h.include_resource(args[0], **kwargs)
return ''


class AssetExtension(BaseExtension):
''' Custom include_asset tag.
{% asset <bundle_name> %}
see lib.webassets_tools.include_asset() for more details.
'''

tags = set(['asset'])

@classmethod
def _call(cls, args, kwargs):
assert len(args) == 1
assert len(kwargs) == 0
h.include_asset(args[0])
return ''


'''
The following function is based on jinja2 code
Expand Down
138 changes: 138 additions & 0 deletions ckan/lib/webassets_tools.py
@@ -0,0 +1,138 @@
# encoding: utf-8

import logging
import os

from markupsafe import Markup

from webassets import Environment
from webassets.loaders import YAMLLoader

from ckan.common import config, g

logger = logging.getLogger(__name__)
env = None


def create_library(name, path):
"""Create WebAssets library(set of Bundles).
"""
config_path = os.path.join(path, u'webassets.yml')
if not os.path.exists(config_path):
return

library = YAMLLoader(config_path).load_bundles()
bundles = {
'/'.join([name, key]): bundle
for key, bundle
in library.items()
}

# Unfortunately, you'll get an error attempting to register bundle
# with the same name twice. For now, let's just pop existing
# bundle and avoid name-conflicts
# TODO: make PR into webassets with preferable solution
for name, bundle in bundles.items():
env._named_bundles.pop(name, None)
env.register(name, bundle)

env.append_path(path)


def webassets_init():
global env

public = config.get(u'ckan.base_public_folder')

public_folder = os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', public))

base_path = os.path.join(public_folder, u'base')
static_path = os.path.join(public_folder, u'webassets')

env = Environment()
env.directory = static_path
logger.warn(u'DEBUG: %s', config.get(u'debug', False))
env.debug = True
env.url = u'/webassets/'

env.append_path(base_path, u'/base/')

logger.debug(u'Base path {0}'.format(base_path))
create_library(u'vendor', os.path.join(
base_path, u'vendor'))

create_library(u'base', os.path.join(base_path, u'javascript'))

create_library(u'datapreview', os.path.join(base_path, u'datapreview'))

create_library(u'css', os.path.join(base_path, u'css'))


def _make_asset_collection():
return {u'style': [], u'script': [], u'included': set()}


def include_asset(name):
try:
if not g.webassets:
raise AttributeError(u'WebAssets not initialized yet')
except AttributeError:
g.webassets = _make_asset_collection()
if name in g.webassets[u'included']:
return

try:
bundle = env[name]
except KeyError:
logger.error(u'Trying to include unknown asset: <{}>'.format(name))
return

deps = bundle.extra.get(u'preload', [])

# Using DFS may lead to infinite recursion(unlikely, because
# extensions rarely depends on each other), so there is a sense to
# memoize visited routes.

# TODO: consider infinite loop prevention for assets that depends
# on each other
for dep in deps:
include_asset(dep)

urls = bundle.urls()
type_ = None
for url in urls:
link = url.split(u'?')[0]
if link.endswith(u'.css'):
type_ = u'style'
break
elif link.endswith(u'.js'):
type_ = u'script'
break
else:
logger.warn(u'Undefined asset type: {}'.format(urls))
return
g.webassets[type_].extend(urls)
g.webassets[u'included'].add(name)


def _to_tag(url, type_):
if type_ == u'style':
return u'<link href="{}" rel="stylesheet"/>'.format(url)
elif type_ == u'script':
return u'<script src="{}" type="text/javascript"></script>'.format(url)
return u''


def render_assets(type_):
try:
assets = g.webassets
except AttributeError:
return u''

if not assets:
return u''
collection = assets[type_]
tags = u'\n'.join([_to_tag(asset, type_) for asset in assets[type_]])
collection[:] = []
return Markup(tags)
27 changes: 18 additions & 9 deletions ckan/plugins/toolkit.py
Expand Up @@ -337,8 +337,10 @@ def _add_served_directory(cls, config, relative_path, config_var):
assert config_var in ('extra_template_paths', 'extra_public_paths')
# we want the filename that of the function caller but they will
# have used one of the available helper functions
frame, filename, line_number, function_name, lines, index =\
inspect.getouterframes(inspect.currentframe())[2]
# TODO: starting from python 3.5, `inspect.stack` returns list
# of named tuples `FrameInfo`. Don't forget to remove
# `getframeinfo` wrapper after migration.
filename = inspect.getframeinfo(inspect.stack()[2][0]).filename

this_dir = os.path.dirname(filename)
absolute_path = os.path.join(this_dir, relative_path)
Expand All @@ -350,24 +352,31 @@ def _add_served_directory(cls, config, relative_path, config_var):

@classmethod
def _add_resource(cls, path, name):
'''Add a Fanstatic resource library to CKAN.
'''Add a WebAssets library to CKAN.
Fanstatic libraries are directories containing static resource files
(e.g. CSS, JavaScript or image files) that can be accessed from CKAN.
WebAssets libraries are directories containing static resource
files (e.g. CSS, JavaScript or image files) that can be
compiled into WebAsset Bundles.
See :doc:`/theming/index` for more details.
'''
import inspect
import os
from ckan.lib.webassets_tools import create_library

# we want the filename that of the function caller but they will
# have used one of the available helper functions
frame, filename, line_number, function_name, lines, index =\
inspect.getouterframes(inspect.currentframe())[1]
# we want the filename that of the function caller but they
# will have used one of the available helper functions
# TODO: starting from python 3.5, `inspect.stack` returns list
# of named tuples `FrameInfo`. Don't forget to remove
# `getframeinfo` wrapper after migration.
filename = inspect.getframeinfo(inspect.stack()[1][0]).filename

this_dir = os.path.dirname(filename)
absolute_path = os.path.join(this_dir, path)
create_library(name, absolute_path)

# TODO: remove next two lines after dropping Fanstatic support
import ckan.lib.fanstatic_resources
ckan.lib.fanstatic_resources.create_library(name, absolute_path)

Expand Down

0 comments on commit 645828f

Please sign in to comment.