Example #1
0
class DelogX(object):
    '''The entry and interface of DelogX.

    Attributes:

        framework (Flask): Flask application object.
        config (Config): Config manager of DelogX.
        runtime (Config): Runtime config manager of DelogX.
        header (list of Page): Pages shown in navbar.
        post_bundle (Bundle): Item bundle of DelogX posts.
        page_bundle (Bundle): Item bundle of DelogX pages.
        observer (Observer): Observer of watchdog module.
        i18n (I18n): I18n manager of DelogX.
        markdown_ext (list of str/object): Extensions of markdown module.
        plugin_manager (PluginManager): Plugin manager of DelogX.
    '''

    def __init__(self, app_path, framework, config='config.json'):
        '''Initialize DelogX object.

        Args:

            app_path (str): Absolute path of the blog application.
            framework (Flask): Flask application object.
            config (str): Config file of DelogX, defaults `config.json`.
        '''
        self.framework = framework
        app_path = os.path.realpath(app_path)
        config = Path.abs_path(app_path, config)
        config = Config(config)
        runtime = Config()
        module_path = os.path.dirname(os.path.realpath(__file__))
        runtime.let('path.app', app_path)
        runtime.let('path.module', module_path)
        ver_file = os.path.join(module_path, 'VERSION')
        with codecs.open(ver_file, encoding='utf-8') as f:
            version = ''.join(f.readlines()).strip()
        runtime.let('blog.version', version)
        self.config = config
        self.runtime = runtime
        self.markdown_ext = list()
        self.init_runtime()
        self.init_plugins()
        self.init_bundles()
        self.init_route()
        self.update_header()

    def init_runtime(self):
        '''Initialize runtime environments of DelogX.'''
        conf = self.default_conf
        runtime = self.runtime
        app_path = runtime.get('path.app')
        module_path = runtime.get('path.module')
        init_path_list = [
            'directory.post',
            'directory.page',
            'directory.static',
            'directory.themes',
            'directory.plugins'
        ]
        init_url_list = [
            'url_prefix.post',
            'url_prefix.page',
            'url_prefix.static',
            'url_prefix.post_list'
        ]
        for key in init_path_list:
            runtime.let(key, Path.abs_path(app_path, conf(key)))
        for key in init_url_list:
            runtime.let(key, Path.format_url(conf(key)))
        themes_dir = runtime.get('directory.themes')
        theme = conf('local.theme')
        theme = 'default' if not theme else theme
        theme_path = os.path.join(themes_dir, theme)
        self.framework.template_folder = theme_path
        self.i18n = I18n(
            Path.format_url(module_path, 'locale'), conf('local.locale'))

    def init_bundles(self):
        '''Initialize bundles of posts and pages.'''
        conf = self.default_conf
        runtime = self.runtime
        post_dir = runtime.get('directory.post')
        page_dir = runtime.get('directory.page')
        self.post_bundle = PostBundle(self)
        self.page_bundle = PageBundle(self)
        post_watch = Watch(self, self.post_bundle, ['*.md'])
        page_watch = Watch(self, self.page_bundle, ['*.md'], is_page=True)
        watch_polling = conf('local.watch_polling')
        self.observer = PollingObserver() if watch_polling else Observer()
        self.observer.setDaemon(True)
        self.observer.schedule(post_watch, post_dir)
        self.observer.schedule(page_watch, page_dir)
        self.observer.start()

    def init_route(self):
        '''Initialize URL routes of DelogX.'''
        runtime = self.runtime
        static_rule = Path.format_url(
            runtime.get('url_prefix.static'), '<static_file>')
        list_rule = Path.format_url(
            runtime.get('url_prefix.post_list'), '<int:number>/')
        item_rule = Path.format_url(
            runtime.get('url_prefix.page'), '<item_id>/')
        page_rule = Path.format_url(
            runtime.get('url_prefix.page'), '<page_id>/')
        post_rule = Path.format_url(
            runtime.get('url_prefix.post'), '<post_id>/')
        icon_rule = Path.format_url(
            runtime.get('url_prefix.static'), 'favicon.ico')
        self.add_url_rule('/', 'delogx_index', self.route_index)
        self.add_url_rule(static_rule, 'delogx_static', self.route_static)
        self.add_url_rule(list_rule, 'delogx_list', self.route_list)
        if runtime.get('url_prefix.post') == runtime.get('url_prefix.page'):
            self.add_url_rule(item_rule, 'delogx_page', self.route_item)
        else:
            self.add_url_rule(page_rule, 'delogx_page', self.route_page)
            self.add_url_rule(post_rule, 'delogx_post', self.route_post)
        self.framework.add_url_rule(
            '/favicon.ico', 'delogx_favicon', redirect_to=icon_rule)
        self.framework.errorhandler(404)(self.route_not_found)

    def init_plugins(self):
        '''Initialize plugin manager of DelogX.'''
        plugins_dir = self.runtime.get('directory.plugins')
        self.plugin_manager = PluginManager(self, plugins_dir)
        manager = self.plugin_manager
        manager.add_filter('dx_page', self.cook_page, 0)
        manager.add_filter('dx_post', self.cook_post, 0)
        manager.add_filter('dx_static', self.cook_static, 0)
        manager.load_all()
        manager.enable_all()

    def default_conf(self, key):
        '''Get a config with default value.

        Args:

            key (str): Key of the config entry.

        Returns:

            object: Value of the config entry.
        '''
        conf = self.config.get
        default_dict = {
            'site.name': 'DelogX',
            'site.subname': 'Yet another Markdown based blog',
            'local.theme': 'default',
            'local.locale': 'en_US',
            'local.list_size': 10,
            'local.watch_polling': False,
            'local.time_format': '%Y-%m-%d %H:%M',
            'url_prefix.post': '/post',
            'url_prefix.page': '/page',
            'url_prefix.post_list': '/list',
            'url_prefix.static': '/static',
            'directory.post': 'posts',
            'directory.page': 'pages',
            'directory.static': 'static',
            'directory.themes': 'themes',
            'directory.plugins': 'plugins',
            'static.css': [],
            'static.js': [],
            'debug.host': '0.0.0.0',
            'debug.port': 8000
        }
        return default_dict.get(key) if conf(key) is None else conf(key)

    def add_url_rule(self, rule, endpoint, func):
        '''Add an URL route rule to Flask object.

        Args:

            rule (str): URL rule string.
            endpoint (str): Endpoint for the registered URL rule.
            func (function): Function to call when request.
        '''
        self.framework.add_url_rule(rule, endpoint, func)

    def update_header(self):
        '''Reload pages shown in navbar.

        Hidden pages will be ignored.
        '''
        header = self.page_bundle.get_list()
        if header is None:
            header = list()
        for page in header:
            page = self.plugin_manager.do_filter('dx_page', page)
        self.plugin_manager.do_action('dx_header')
        self.header = self.plugin_manager.do_filter('dx_header', header)

    def get_render(self, template, **context):
        '''Return rendered HTML content response.

        Args:

            template (str): Filename of the template.
            **context: Context of the template.

        Returns:

            Response: rendered HTML response.

        Contexts:

            app: DelogX itself.
            _g: Method to get i18n text.
            _c: Method to get config.
            _rt: Method to get runtime config.
            _css: CSS list of site.
            _js: JS list of site.
        '''
        conf = self.default_conf
        runtime = self.runtime
        render = render_template(
            template,
            app=self,
            _g=self.i18n.get,
            _c=self.default_conf,
            _rt=self.runtime.get,
            _css=self.get_static(conf('static.css')),
            _js=self.get_static(conf('static.js')),
            **context)
        self.plugin_manager.do_action('dx_render')
        render = self.plugin_manager.do_filter('dx_render', render)
        resp = make_response(render)
        resp.headers['DelogX-Version'] = runtime.get('blog.version')
        return resp

    def get_page(self, page_id):
        '''Return cooked Page object by id.

        Args:

            page_id (str): Request ID of the page.

        Returns:

            Page: Cooked Page object.
        '''
        page_id = Path.urldecode(page_id)
        page = self.page_bundle.get(page_id)
        self.plugin_manager.do_action('dx_page')
        page = self.plugin_manager.do_filter('dx_page', page)
        return page

    def cook_page(self, page):
        '''Cook a Page.

        Add prefix to the URL of the Page.

        Args:

            page (Page): Page needs to cook.

        Returns:

            Page: Cooked Page.
        '''
        runtime = self.runtime.get
        if not page:
            return None
        page_url = runtime('url_prefix.page')
        page.cooked_url = Path.format_url(page_url, Path.urlencode(page.url))
        return page

    def get_post(self, post_id):
        '''Return cooked Post object by id.

        Args:

            post_id (str): Request ID of the post.

        Returns:

            Post: Cooked Post object.
        '''
        post_id = Path.urldecode(post_id)
        post = self.post_bundle.get(post_id)
        self.plugin_manager.do_action('dx_post')
        return self.plugin_manager.do_filter('dx_post', post)

    def cook_post(self, post):
        '''Cook a Post.

        Add prefix to the URL of the Post and convert timestamp to string.

        Args:

            post (Post): Post needs to cook.

        Returns:

            Post: Cooked Post.
        '''
        conf = self.config.get
        runtime = self.runtime.get
        if not post:
            return None
        post_url = runtime('url_prefix.post')
        time_format = conf('local.time_format')
        post.cooked_url = Path.format_url(post_url, Path.urlencode(post.url))
        post.cooked_time = time.strftime(
            time_format, time.localtime(post.time))
        post.cooked_time = post.cooked_time
        return post

    def get_static(self, statics):
        '''Return a list of static files.

        Args:

            statics (list of str): Static files.

        Returns:

            list: Cooked static files.
        '''
        self.plugin_manager.do_action('dx_static')
        return self.plugin_manager.do_filter('dx_static', statics)

    def cook_static(self, statics):
        '''Cook a list of static files.

        Convert all relative links to absolute.

        Args:

            statics (list of str): Static files.

        Returns:

            list: Cooked list of static files.
        '''
        runtime = self.runtime.get
        static_url = runtime('url_prefix.static')
        if not statics:
            statics = list()
        for i, link in enumerate(statics):
            if not link.startswith(('/', 'http://', 'https://')):
                statics[i] = Path.format_url(static_url, link)
        return statics

    def route_static(self, static_file):
        '''Response a static file.

        Search the file in static directory firstly, then search in
        theme directory.

        Args:

            static_file (str): Relative path of the static file.

        Returns:

            object: Static file.

        Abort:

            404: No such static file.
        '''
        static_dir = self.runtime.get('directory.static')
        themes_dir = self.framework.template_folder
        static_path = os.path.join(static_dir, static_file)
        themes_path = os.path.join(themes_dir, 'static', static_file)
        if os.path.isfile(static_path):
            return send_from_directory(
                os.path.dirname(static_path),
                os.path.basename(static_path))
        elif os.path.isfile(themes_path):
            return send_from_directory(
                os.path.dirname(themes_path),
                os.path.basename(themes_path))
        abort(404)

    def route_index(self):
        '''Response the home page.

        Home page, i.e. the first page of posts.

        Returns:

            str: Rendered HTML of the home page.
        '''
        return self.route_list(1)

    def route_list(self, number):
        '''Response a list of posts.

        Hidden posts will be ignored.

        Args:

            number (int): Page number of the list.

        Returns:

            str: Rendered HTML of the posts list.

        Abort:

            404: No such page of posts list.
        '''
        runtime = self.runtime.get
        post_count = self.post_bundle.get_list_count()
        post_list = self.post_bundle.get_list(number)
        if post_list is None:
            abort(404)
        for post in post_list:
            post = self.plugin_manager.do_filter('dx_post', post)
        prev_page = next_page = True
        if number == 1:
            prev_page = False
        if number == post_count:
            next_page = False
        list_url = Path.format_url(runtime('url_prefix.post_list'))
        url = list_url if list_url.endswith('/') else list_url + '/'
        return self.get_render(
            'list.html', posts=post_list,
            list_id=number, list_url=url,
            prev_page=prev_page, next_page=next_page)

    def route_page(self, page_id):
        '''Response a page.

        Args:

            page_id (str): Request ID of the page.

        Returns:

            str: Rendered HTML of the page.

        Abort:

            404: No such page.
        '''
        page = self.get_page(page_id)
        if page:
            return self.get_render('page.html', page=page)
        abort(404)

    def route_post(self, post_id):
        '''Response a post.

        Args:

            post_id (str): Request ID of the post.

        Returns:

            str: Rendered HTML of the post.

        Abort:

            404: No such post.
        '''
        post = self.get_post(post_id)
        if post:
            return self.get_render('post.html', post=post)
        abort(404)

    def route_item(self, item_id):
        '''Response a page or post.

        Args:

            item_id (str): Request ID of the item.

        Returns:

            str: Rendered HTML of item.

        Abort:

            404: No such item.
        '''
        page = self.get_page(item_id)
        post = self.get_post(item_id)
        if page:
            return self.get_render('page.html', page=page)
        elif post:
            return self.get_render('post.html', post=post)
        abort(404)

    def route_not_found(self, error):
        '''Response `404 Not Found` page.

        Args:

            error (object): Error code of the request.

        Returns:

            str: Rendered HTML of 404.
        '''
        return self.get_render('404.html'), error.code