def start(self): """Go through the boot up process sending signals for each stage.""" super(MDSite, self).__init__(self.site_name, **self.app_options) #: START THE BOOT PROCESS MDW_SIGNALER['pre-boot'].send(self) self._stage_pre_boot() #: START THE FLASK APP # We must start the app straight away because we can't get the config # easily until we do. The rest of the boot tasks will require the # config. MDW_SIGNALER['pre-app-start'].send(self) self._stage_create_app() MDW_SIGNALER['post-app-start'].send(self) #: LOAD THE CONFIG MDW_SIGNALER['pre-config'].send(self) self._stage_load_config() MDW_SIGNALER['post-config'].send(self) #: SETUP NAVIGATION MDW_SIGNALER['pre-navigation-scan'].send(self) self.navigation = Navigation(self.config['CONTENT_PATH']) self.pages = self.navigation.get_page_dict() self.context_processor(self._inject_navigation) MDW_SIGNALER['post-navigation-scan'].send(self) #: FINISH THINGS UP self._stage_post_boot() MDW_SIGNALER['post-boot'].send(self)
def test_get_page_dict(self): """The method should return a flattend dictionary of all pages.""" self.fs.CreateFile('/my/content/index.md') self.fs.CreateFile('/my/content/about/index.md') self.fs.CreateFile('/my/content/contact/index.md') self.fs.CreateFile('/my/content/contact/westcoast.md') self.fs.CreateFile('/my/content/contact/eastcoast.md') self.fs.CreateFile('/my/content/work/portfolio/index.md') self.fs.CreateFile('/my/content/work/portfolio/landscapes.md') self.fs.CreateFile('/my/content/work/portfolio/portraits.md') self.fs.CreateFile('/my/content/work/portfolio/nature.md') self.fs.CreateFile('/my/content/order/digitalprints.md') self.fs.CreateFile('/my/content/order/framed.md') nav = Navigation('/my/content') page_dict = nav.get_page_dict() self.assertEqual(page_dict[''].page_path, '/my/content/index.md') self.assertEqual(page_dict['contact'].page_path, '/my/content/contact/index.md') self.assertEqual(page_dict['contact/westcoast'].page_path, '/my/content/contact/westcoast.md') self.assertEqual(page_dict['contact/eastcoast'].page_path, '/my/content/contact/eastcoast.md') self.assertEqual(page_dict['work/portfolio'].page_path, '/my/content/work/portfolio/index.md') self.assertEqual(page_dict['work/portfolio/landscapes'].page_path, '/my/content/work/portfolio/landscapes.md') self.assertEqual(page_dict['work/portfolio/portraits'].page_path, '/my/content/work/portfolio/portraits.md') self.assertEqual(page_dict['work/portfolio/nature'].page_path, '/my/content/work/portfolio/nature.md') self.assertEqual(page_dict['order/digitalprints'].page_path, '/my/content/order/digitalprints.md') self.assertEqual(page_dict['order/framed'].page_path, '/my/content/order/framed.md')
class MDSite(Flask): """MDWeb site. An MDWeb Site is very closely related to a Flask application. """ #: Assets that must live at the root level and thus require special routing ROOT_LEVEL_ASSETS = [ 'crossdomain.xml', 'favicon.ico', 'humans.txt', 'robots.txt', ] #: Navigation structure navigation = None # pylint: disable=W0231 def __init__(self, site_name, app_options=None, site_options=None): """Initialize the Flask application and start the app. :param site_name: The name of the site, will be used as the flask import_name. :param app_options: Additional parameters to be passed to Flask constructor :param site_options: Options specific to MDWeb sites """ self.site_name = site_name self.app_options = {} if app_options is None else app_options self.site_options = BASE_SITE_OPTIONS self.site_options.update({} if site_options is None else site_options) self.pages = [] self.content_observer = None self.theme_observer = None self.navigation = None self.start() if not self.config['TESTING']: self._register_observers() def start(self): """Go through the boot up process sending signals for each stage.""" super(MDSite, self).__init__(self.site_name, **self.app_options) #: START THE BOOT PROCESS MDW_SIGNALER['pre-boot'].send(self) self._stage_pre_boot() #: START THE FLASK APP # We must start the app straight away because we can't get the config # easily until we do. The rest of the boot tasks will require the # config. MDW_SIGNALER['pre-app-start'].send(self) self._stage_create_app() MDW_SIGNALER['post-app-start'].send(self) #: LOAD THE CONFIG MDW_SIGNALER['pre-config'].send(self) self._stage_load_config() MDW_SIGNALER['post-config'].send(self) #: SETUP NAVIGATION MDW_SIGNALER['pre-navigation-scan'].send(self) self.navigation = Navigation(self.config['CONTENT_PATH']) self.pages = self.navigation.get_page_dict() self.context_processor(self._inject_navigation) MDW_SIGNALER['post-navigation-scan'].send(self) #: FINISH THINGS UP self._stage_post_boot() MDW_SIGNALER['post-boot'].send(self) def get_page(self, url_path): """Lookup the page for the given url path. :param url_path: :return: Page object matching the requested url path """ for page_url, page in self.pages.items(): if page.url_path == url_path: return page return None def error_page_not_found(self, error): """Show custom 404 page. :param e: """ # TODO: Make this use the 404.md return "404 - TODO: Make this use the 404.md", 404 def _register_observers(self): """Setup a watcher to rebuild the nav whenever a file has changed.""" _this = self class ContentHandler(FileSystemEventHandler): """Custom event handler for changed files.""" def on_modified(self, event): logging.debug('%s "%s" was "%s"', 'Directory' if event.is_directory else "File", event.src_path, event.event_type) _this.start() event_handler = ContentHandler() # Listen for content changes self.content_observer = Observer() self.content_observer.schedule(event_handler, self.config['CONTENT_PATH'], recursive=True) self.content_observer.start() # If we're debugging, listen for theme changes if self.debug: self.theme_observer = Observer() self.theme_observer.schedule(event_handler, self.config['THEME_FOLDER'], recursive=True) self.theme_observer.start() def _stage_pre_boot(self): """Do pre-boot tasks.""" pass def _stage_create_app(self): """Create the Flask application.""" # Setup special root-level asset routes # NOTE: The usage of url_for doesn't work here. Rather, use a view with # send_from_directory() - http://stackoverflow.com/a/20648053/1436323 def special_root_file(filename): """Root file Flask view.""" return send_file(os.path.join(self.config['CONTENT_PATH'], filename)) for asset in self.ROOT_LEVEL_ASSETS: self.add_url_rule('/%s' % asset, view_func=special_root_file, defaults={'filename': asset}) # Setup content asset route def custom_static(filename): """Custom static file Flask view.""" return send_from_directory(self.config['CONTENT_ASSET_PATH'], filename) self.add_url_rule('/contentassets/<path:filename>', view_func=custom_static) # Sitemap route self.add_url_rule('/sitemap.xml', view_func=SiteMapView.as_view('sitemap')) # Route all remaining requests to the index view self.add_url_rule('/', view_func=Index.as_view('index'), defaults={'path': ''}) self.add_url_rule('/<path:path>', view_func=Index.as_view('index_with_path')) # Setup error handler pages self.error_handler_spec[None][404] = self.error_page_not_found # Setup logging log_level = getattr(logging, self.site_options['logging_level']) logging.basicConfig(level=log_level, format='%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') def _stage_load_config(self): """Load the configuration of the application being started.""" self_fqcn = self.__module__ + "." + self.__class__.__name__ self.config.from_object('%s.MDConfig' % self_fqcn) path_to_here = os.path.dirname(os.path.realpath(__file__)) self.config['BASE_PATH'] = os.path.abspath(os.path.join(path_to_here, os.pardir)) # Build the full path the the theme folder using the theme name self.config['THEME_FOLDER'] = os.path.join( self.config['BASE_PATH'], 'themes', self.config['THEME'] ) # Ensure theme directory exists if not os.path.isdir(self.config['THEME_FOLDER']) or \ not os.path.exists(self.config['THEME_FOLDER']): raise FileExistsError("Theme directory %s does not exist" % self.config['THEME_FOLDER']) # Set the template directory to the configured theme's template # directory # http://stackoverflow.com/a/13598839 my_loader = jinja2.ChoiceLoader([ self.jinja_loader, jinja2.FileSystemLoader([ os.path.join(self.config['THEME_FOLDER'], 'templates'), ]), ]) self.jinja_loader = my_loader # Extend the content path to the absolute path if not self.config['CONTENT_PATH'].startswith('/'): self.config['CONTENT_PATH'] = os.path.join( self.config['BASE_PATH'], self.config['CONTENT_PATH'] ) # Ensure content directory exists if not os.path.isdir(self.config['CONTENT_PATH']) or \ not os.path.exists(self.config['CONTENT_PATH']): raise FileExistsError("Content directory %s does not exist" % self.config['CONTENT_PATH']) # Set the static theme assets folder path self.static_folder = os.path.join(self.config['THEME_FOLDER'], 'assets') # Set the content asset path self.config['CONTENT_ASSET_PATH'] = os.path.join( self.config['CONTENT_PATH'], 'assets') def _stage_post_boot(self): """Do post-boot tasks.""" pass def _inject_navigation(self): return dict(navigation=self.navigation)