Repository: getnikola/nikola

Issue:
Make Bootstrap 4 the default theme
For Nikola v8.

- [x] Move bootstrap4 to Nikola core, and bootstrap3 to the Themes repo
- [x] Make the bootstrap4 theme independent of bootstrap3
- [x] Get away from bower as it does not support bs4 (#2790)
- [x] Create bootblog4 theme based on [the example](https://getbootstrap.com/docs/4.0/examples/blog/), make that the new default theme
- [x] Get away from `flowr.js` due to no jQuery 3 support
- [x] sourcelink should use a helper to avoid duplicating post.tmpl, listing.tmpl
bootblog4 looks meh on mobile
http://ralsina.me/ ends up like this:

![image](https://user-images.githubusercontent.com/327323/39539777-e90ebd90-4e40-11e8-8d10-88b5e2c5f6a0.png)




Discussion:
+1 to making a bootblog4 and even to making it default
Work is happening on the `bootstrap4` branch.

Bootblog4 now has a regression against other themes: source links are not not visible on mobile. If anyone knows how to implement them cleanly and neatly, patches welcome.

Also welcome: ideas on how to pick featured posts. Special tags? Meta keys? Or just show the latest 3 posts there?
I would add a "featured" tag and just pick the N newest
cc @ralsina
Yes, it is not the prettiest. I suspect the search form needs tweaking, since it was bootstrap3 code originally.
The search form should collapse into the menu, that would make it look ok. I am trying to do it but ... eek CSS.
Blah, the example doesn't even have a search form, just a fake thing.
This is slightly outside my skills at HTTP/CSS/etc. I guess. 

I can ask for help tho.

Resolving patch:
diff --git a/nikola/nikola.py b/nikola/nikola.py
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -570,6 +570,7 @@ def __init__(self, **config):
             'TEMPLATE_FILTERS': {},
             'THEME': LEGAL_VALUES['DEFAULT_THEME'],
             'THEME_COLOR': '#5670d4',  # light "corporate blue"
+            'THEME_CONFIG': {},
             'THUMBNAIL_SIZE': 180,
             'TRANSLATIONS_PATTERN': '{path}.{lang}.{ext}',
             'URL_TYPE': 'rel_path',
@@ -648,6 +649,7 @@ def __init__(self, **config):
                                       'CATEGORY_DESTPATH_NAMES',
                                       'INDEXES_PAGES',
                                       'INDEXES_PRETTY_PAGE_URL',
+                                      'THEME_CONFIG',
                                       # PATH options (Issue #1914)
                                       'TAG_PATH',
                                       'TAGS_INDEX_PATH',
@@ -680,6 +682,7 @@ def __init__(self, **config):
                                              'date_format',
                                              'js_date_format',
                                              'front_index_header',
+                                             'theme_config',
                                              )
 
         self._ALL_PAGE_DEPS_TRANSLATABLE = ('atom_path',
@@ -1143,6 +1146,7 @@ def _set_global_context_from_config(self):
         self._GLOBAL_CONTEXT['use_bundles'] = self.config['USE_BUNDLES']
         self._GLOBAL_CONTEXT['use_cdn'] = self.config.get("USE_CDN")
         self._GLOBAL_CONTEXT['theme_color'] = self.config.get("THEME_COLOR")
+        self._GLOBAL_CONTEXT['theme_config'] = self.config.get("THEME_CONFIG")
         self._GLOBAL_CONTEXT['favicons'] = self.config['FAVICONS']
         self._GLOBAL_CONTEXT['date_format'] = self.config.get('DATE_FORMAT')
         self._GLOBAL_CONTEXT['blog_author'] = self.config.get('BLOG_AUTHOR')
@@ -2480,6 +2484,9 @@ def generic_index_renderer(self, lang, posts, indexes_title, template_name, cont
                    as the ones for page_link.
         additional_dependencies: a list of dependencies which will be added
                                  to task['uptodate']
+
+        Note: if context['featured'] is present, it must be a list of posts,
+        whose dependencies will be taken added to task['uptodate'].
         """
         # Update kw
         kw = kw.copy()
@@ -2581,6 +2588,13 @@ def generic_index_renderer(self, lang, posts, indexes_title, template_name, cont
                     context['current_page'] = ipages_i - 1
                 context['prev_next_links_reversed'] = kw['indexes_static']
             context["permalink"] = page_links[i]
+            context["is_frontmost_index"] = i == 0
+
+            # Add dependencies to featured posts
+            if 'featured' in context:
+                for post in context['featured']:
+                    additional_dependencies += post.deps_uptodate(lang)
+
             output_name = os.path.join(kw['output_folder'], page_path(i, ipages_i, num_pages, False))
             task = self.generic_post_list_renderer(
                 lang,
diff --git a/nikola/plugins/task/indexes.py b/nikola/plugins/task/indexes.py
--- a/nikola/plugins/task/indexes.py
+++ b/nikola/plugins/task/indexes.py
@@ -112,11 +112,14 @@ def get_path(self, classification, lang, dest_type='page'):
     def provide_context_and_uptodate(self, classification, lang, node=None):
         """Provide data for the context and the uptodate list for the list of the given classifiation."""
         kw = {
+            "show_untranslated_posts": self.site.config["SHOW_UNTRANSLATED_POSTS"],
         }
         context = {
             "title": self.site.config["INDEXES_TITLE"](lang) or self.site.config["BLOG_TITLE"](lang),
             "description": self.site.config["BLOG_DESCRIPTION"](lang),
             "pagekind": ["main_index", "index"],
+            "featured": [p for p in self.site.posts if p.post_status == 'featured'
+                         and (lang in p.translated_to or kw["show_untranslated_posts"])],
         }
         kw.update(context)
         return context, kw
diff --git a/nikola/post.py b/nikola/post.py
--- a/nikola/post.py
+++ b/nikola/post.py
@@ -263,9 +263,9 @@ def __init__(
 
         self.publish_later = False if self.current_time is None else self.date >= self.current_time
 
-        is_draft = False
-        is_private = False
-        post_status = 'published'
+        self.is_draft = False
+        self.is_private = False
+        self.post_status = 'published'
         self._tags = {}
         self.has_oldstyle_metadata_tags = False
         for lang in self.translated_to:
@@ -280,16 +280,16 @@ def __init__(
 
             status = self.meta[lang].get('status')
             if status:
-                is_private = False
-                is_draft = False
-                if status in ('published', 'featured'):
-                    post_status = status
+                if status == 'published':
+                    pass  # already set before, mixing published + something else should result in the other thing
+                if status == 'featured':
+                    self.post_status = status
                 elif status == 'private':
-                    post_status = status
-                    is_private = True
+                    self.post_status = status
+                    self.is_private = True
                 elif status == 'draft':
-                    post_status = status
-                    is_draft = True
+                    self.post_status = status
+                    self.is_draft = True
                 else:
                     LOGGER.warn(('The post "{0}" has the unknown status "{1}". '
                                  'Valid values are "published", "featured", "private" and "draft".').format(self.source_path, status))
@@ -313,29 +313,25 @@ def __init__(
                                 'configuration to disable this warning.')
             if self.config['USE_TAG_METADATA']:
                 if 'draft' in [_.lower() for _ in self._tags[lang]]:
-                    is_draft = True
+                    self.is_draft = True
                     LOGGER.debug('The post "{0}" is a draft.'.format(self.source_path))
                     self._tags[lang].remove('draft')
-                    post_status = 'draft'
+                    self.post_status = 'draft'
                     self.has_oldstyle_metadata_tags = True
 
                 if 'private' in self._tags[lang]:
-                    is_private = True
+                    self.is_private = True
                     LOGGER.debug('The post "{0}" is private.'.format(self.source_path))
                     self._tags[lang].remove('private')
-                    post_status = 'private'
+                    self.post_status = 'private'
                     self.has_oldstyle_metadata_tags = True
 
                 if 'mathjax' in self._tags[lang]:
                     self.has_oldstyle_metadata_tags = True
 
         # While draft comes from the tags, it's not really a tag
-        self.is_draft = is_draft
-        self.is_private = is_private
         self.is_post = use_in_feeds
-        self.post_status = post_status
-        self.use_in_feeds = use_in_feeds and not is_draft and not is_private \
-            and not self.publish_later
+        self.use_in_feeds = self.is_post and not self.is_draft and not self.is_private and not self.publish_later
 
         # Allow overriding URL_TYPE via meta
         # The check is done here so meta dicts won’t change inside of


Test patch: