Implement extra header support and multi author support

Also add a lot of parenthesis
This commit is contained in:
Mikko Ahlroth 2016-02-18 23:12:39 +02:00
parent 7e6b490465
commit b33c48c8db
21 changed files with 298 additions and 74 deletions

View file

@ -23,6 +23,9 @@ config :mebe_web,
blog_author: "Author McAuthor",
absolute_url: "http://localhost:4000", # Absolute URL to the site without trailing slash, including protocol
multi_author_mode: false, # Set to true to show author header from posts, if available (blog_author will be used as default)
use_default_author: true, # If multi author mode is on, use blog_author as default author (if this is false, no author will be set if post has no author header)
force_read_more: false, # Force "Read more…" text to display even if there is no more content
enable_feeds: false, # Set to true to enable RSS feeds

View file

@ -10,6 +10,8 @@ defmodule MebeEngine.Crawler do
alias MebeEngine.Models.Page
alias MebeEngine.Models.Post
alias MebeWeb.Utils
def crawl(path) do
get_files(path)
|> Enum.map(fn file -> Task.async MebeEngine.Crawler, :parse, [file] end)
@ -38,16 +40,30 @@ defmodule MebeEngine.Crawler do
end
def construct_archives(datalist) do
Enum.reduce datalist, %{pages: %{}, posts: [], years: %{}, months: %{}, tags: %{}}, fn pagedata, acc ->
multi_author_mode = Utils.get_conf(:multi_author_mode)
Enum.reduce datalist, %{pages: %{}, posts: [], years: %{}, months: %{}, tags: %{}, authors: %{}, author_names: %{}}, fn pagedata, acc ->
case pagedata.__struct__ do
Page -> %{acc | pages: Map.put(acc.pages, pagedata.slug, pagedata)}
Post ->
{year, month, _} = pagedata.date
tags = Enum.reduce pagedata.tags, acc.tags, fn tag, tagmap ->
tags = Enum.reduce(pagedata.tags, acc.tags, fn tag, tagmap ->
posts = Map.get(tagmap, tag, [])
Map.put(tagmap, tag, [pagedata | posts])
end)
authors = %{}
author_names = %{}
if multi_author_mode do
author_name = Utils.get_author(pagedata)
author_slug = Utils.slugify(author_name)
author_posts = [pagedata | Map.get(acc.authors, author_slug, [])]
authors = Map.put(acc.authors, author_slug, author_posts)
# Authors end up with the name that was in the post with the first matching slug
author_names = Map.put_new(acc.author_names, author_slug, author_name)
end
year_posts = [pagedata | Map.get(acc.years, year, [])]
@ -58,7 +74,9 @@ defmodule MebeEngine.Crawler do
posts: [pagedata | acc.posts],
years: Map.put(acc.years, year, year_posts),
months: Map.put(acc.months, {year, month}, month_posts),
tags: tags
tags: tags,
authors: authors,
author_names: author_names
}
end
end

View file

@ -1,9 +1,13 @@
defmodule MebeEngine.DB do
require Logger
alias MebeWeb.Utils
@moduledoc """
Stuff related to storing the blog data to memory (ETS).
"""
# Table for meta information, like the counts of posts
# Table for meta information, like the counts of posts and names
# of authors
@meta_table :mebeweb_meta
# Table for storing pages by slug
@ -18,6 +22,9 @@ defmodule MebeEngine.DB do
# Table for storing posts with tag as first element of key
@tag_table :mebeweb_tags
# Table for storing posts by specific authors
@author_table :mebeweb_authors
# Table for storing menu data
@menu_table :mebeweb_menu
@ -30,6 +37,10 @@ defmodule MebeEngine.DB do
:ets.new @single_post_table, [:named_table, :set, :protected, read_concurrency: true]
:ets.new @tag_table, [:named_table, :ordered_set, :protected, read_concurrency: true]
:ets.new @menu_table, [:named_table, :ordered_set, :protected, read_concurrency: true]
if Utils.get_conf(:multi_author_mode) do
:ets.new @author_table, [:named_table, :ordered_set, :protected, read_concurrency: true]
end
end
end
@ -41,8 +52,12 @@ defmodule MebeEngine.DB do
:ets.delete_all_objects @tag_table
end
def insert_count(key, count) do
:ets.insert @meta_table, {key, count}
def insert_count(:all, count) do
insert_meta(:all, :all, count)
end
def insert_count(type, key, count) do
insert_meta(type, key, count)
end
def insert_menu(menu) do
@ -61,18 +76,55 @@ defmodule MebeEngine.DB do
{year, month, day} = post.date
{{year, month, day, post.slug}, post}
end
:ets.insert @post_table, ordered_posts
:ets.insert @single_post_table, single_posts
if Utils.get_conf(:multi_author_mode) do
author_posts = Enum.filter(posts, fn post -> Map.has_key?(post.extra_headers, "author") end)
|> Enum.map(fn post ->
{year, month, day} = post.date
author_slug = Utils.get_author(post) |> Utils.slugify
{{author_slug, year, month, day, post.order}, post}
end)
:ets.insert @author_table, author_posts
end
end
def insert_page(page) do
:ets.insert @page_table, {page.slug, page}
end
def insert_tag_post(tag, post) do
{year, month, day} = post.date
:ets.insert @tag_table, {{tag, year, month, day, post.order}, post}
def insert_tag_posts(tags) do
tag_posts = Enum.reduce(Map.keys(tags), [], fn tag, acc ->
Enum.reduce(tags[tag], acc, fn post, inner_acc ->
{year, month, day} = post.date
[{{tag, year, month, day, post.order}, post} | inner_acc]
end)
end)
:ets.insert @tag_table, tag_posts
end
def insert_author_posts(authors) do
author_posts = Enum.reduce(Map.keys(authors), [], fn author_slug, acc ->
Enum.reduce(authors[author_slug], acc, fn post, inner_acc ->
{year, month, day} = post.date
[{{author_slug, year, month, day, post.order}, post} | inner_acc]
end)
end)
:ets.insert @author_table, author_posts
end
def insert_author_names(author_names_map) do
author_names = Enum.reduce(Map.keys(author_names_map), [], fn author_slug, acc ->
[{{:author_name, author_slug}, author_names_map[author_slug]} | acc]
end)
:ets.insert @meta_table, author_names
end
@ -84,19 +136,23 @@ defmodule MebeEngine.DB do
end
def get_reg_posts(first, last) do
get_post_list @post_table, [{:"$1", [], [:"$_"]}], first, last
get_post_list(@post_table, [{:"$1", [], [:"$_"]}], first, last)
end
def get_tag_posts(tag, first, last) do
get_post_list @tag_table, [{{{tag, :_, :_, :_, :_}, :"$1"}, [], [:"$_"]}], first, last
get_post_list(@tag_table, [{{{tag, :_, :_, :_, :_}, :"$1"}, [], [:"$_"]}], first, last)
end
def get_author_posts(author_slug, first, last) do
get_post_list(@author_table, [{{{author_slug, :_, :_, :_, :_}, :"$1"}, [], [:"$_"]}], first, last)
end
def get_year_posts(year, first, last) do
get_post_list @post_table, [{{{year, :_, :_, :_}, :"$1"}, [], [:"$_"]}], first, last
get_post_list(@post_table, [{{{year, :_, :_, :_}, :"$1"}, [], [:"$_"]}], first, last)
end
def get_month_posts(year, month, first, last) do
get_post_list @post_table, [{{{year, month, :_, :_}, :"$1"}, [], [:"$_"]}], first, last
get_post_list(@post_table, [{{{year, month, :_, :_}, :"$1"}, [], [:"$_"]}], first, last)
end
def get_page(slug) do
@ -113,16 +169,33 @@ defmodule MebeEngine.DB do
end
end
def get_count(type) do
case :ets.match_object @meta_table, {type, :"$1"} do
[{_, count}] -> count
[] -> 0
end
def get_count(:all) do
get_count(:all, :all)
end
def get_count(type, key) do
get_meta(type, key, 0)
end
def get_author_name(author_slug) do
get_meta(:author_name, author_slug, author_slug)
end
defp insert_meta(type, key, value) do
:ets.insert @meta_table, {{type, key}, value}
end
defp get_meta(type, key, default) do
case :ets.match_object @meta_table, {{type, key}, :"$1"} do
[{{_, _}, value}] -> value
[] -> default
end
end
# Combine error handling of different post listing functions
defp get_post_list(table, matchspec, first, last) do
case :ets.select_reverse table, matchspec, first + last do

View file

@ -1,6 +1,6 @@
defmodule MebeEngine.Models do
@moduledoc """
This module contains the data models (not db models) of the blog engine.
This module contains the data models of the blog engine.
"""
defmodule PageData do
@ -18,13 +18,15 @@ defmodule MebeEngine.Models do
content: nil,
short_content: nil,
order: 0,
has_more: false
has_more: false,
extra_headers: %{}
end
defmodule Page do
defstruct slug: nil,
title: nil,
content: nil
content: nil,
extra_headers: %{}
end
defmodule MenuItem do

View file

@ -59,11 +59,12 @@ defmodule MebeEngine.Parser do
%Page{
slug: slug,
title: title,
content: content
content: content,
extra_headers: parse_headers(headers)
}
[_, year, month, day, order, slug] ->
[tags | _] = headers
[tags | extra_headers] = headers
order = format_order order
@ -77,12 +78,27 @@ defmodule MebeEngine.Parser do
content: content,
short_content: hd(split_content),
order: order,
has_more: (Enum.count(split_content) > 1)
has_more: (Enum.count(split_content) > 1),
extra_headers: parse_headers(extra_headers)
}
end
end
defp parse_headers(headers) do
# Parse a list of headers into a string keyed map
Enum.reduce(headers, %{}, fn header, acc ->
{key, val} = split_header(header)
Map.put(acc, key, val)
end)
end
defp split_header(header) do
# Enforce 2 parts
[key | [val]] = String.split(header, ":", parts: 2)
{String.strip(key), String.strip(val)}
end
defp parse_tags(tagline) do
String.split tagline, ~R/,\s*/iu
end

View file

@ -36,42 +36,47 @@ defmodule MebeEngine.Worker do
Initialize the database by crawling the configured path and parsing data to the DB.
"""
def load_db() do
data_path = Utils.get_conf :data_path
data_path = Utils.get_conf(:data_path)
Logger.info "Loading menu from '#{data_path}/menu'…"
Logger.info("Loading menu from '#{data_path}/menu'…")
menu = MenuParser.parse data_path
menu = MenuParser.parse(data_path)
Logger.info "Loading post database from '#{data_path}'…"
Logger.info("Loading post database from '#{data_path}'…")
%{
pages: pages,
posts: posts,
tags: tags,
authors: authors,
author_names: author_names,
years: years,
months: months,
} = Crawler.crawl data_path
} = Crawler.crawl(data_path)
Logger.info "Loaded #{Enum.count pages} pages and #{Enum.count posts} posts."
Logger.info("Loaded #{Enum.count pages} pages and #{Enum.count posts} posts.")
DB.init
DB.init()
DB.insert_menu menu
DB.insert_posts posts
DB.insert_count :all, Enum.count posts
DB.insert_menu(menu)
DB.insert_posts(posts)
DB.insert_count(:all, Enum.count(posts))
Enum.each Map.keys(pages), fn page -> DB.insert_page pages[page] end
Enum.each Map.keys(tags),
fn tag ->
Enum.each(tags[tag], fn post -> DB.insert_tag_post(tag, post) end)
DB.insert_count tag, Enum.count tags[tag]
end
DB.insert_tag_posts(tags)
Enum.each(Map.keys(tags), fn tag -> DB.insert_count(:tag, tag, Enum.count(tags[tag])) end)
if Utils.get_conf(:multi_author_mode) do
DB.insert_author_posts(authors)
DB.insert_author_names(author_names)
Enum.each(Map.keys(authors), fn author -> DB.insert_count(:author, author, Enum.count(authors[author])) end)
end
# For years and months, only insert the counts (the data can be fetched from main posts table)
Enum.each Map.keys(years), fn year -> DB.insert_count year, Enum.count years[year] end
Enum.each Map.keys(months), fn month -> DB.insert_count month, Enum.count months[month] end
Enum.each(Map.keys(years), fn year -> DB.insert_count(:year, year, Enum.count(years[year])) end)
Enum.each(Map.keys(months), fn month -> DB.insert_count(:month, month, Enum.count(months[month])) end)
Logger.info "Posts loaded."
Logger.info("Posts loaded.")
end
end

View file

@ -1,4 +1,7 @@
defmodule MebeWeb.Utils do
alias MebeEngine.Models
alias MebeEngine.DB
@moduledoc """
This module contains functions and other stuff that just don't fit anywhere else properly.
"""
@ -12,4 +15,57 @@ defmodule MebeWeb.Utils do
def get_conf(key) do
Application.get_env :mebe_web, key
end
@doc """
Get the author of a post.
Returns a value according to the following pseudocode
if multi author mode is on then
if post has author then
return post's author
else if use default author is on then
return blog author
else return nil
else if use default author is on then
return blog author
else return nil
"""
def get_author(post = %Models.Post{}) do
multi_author_mode = get_conf(:multi_author_mode)
use_default_author = get_conf(:use_default_author)
blog_author = get_conf(:blog_author)
if multi_author_mode do
cond do
Map.has_key?(post.extra_headers, "author") ->
Map.get(post.extra_headers, "author")
use_default_author ->
blog_author
true -> nil
end
else
if use_default_author, do: blog_author, else: nil
end
end
@doc """
Get slug out of a given value.
Nil is returned as is.
"""
def slugify(nil), do: nil
def slugify(value) do
Slugger.slugify_downcase(value)
end
@doc """
Get the author name related to this slug from the DB.
"""
def unslugify_author(slug) do
DB.get_author_name(slug)
end
end

View file

@ -35,7 +35,8 @@ defmodule MebeWeb.Mixfile do
{:cowboy, "~> 1.0"},
{:earmark, "~> 0.2.0"},
{:exrm, "~> 1.0.0-rc5"},
{:conform, "~> 1.0.0-rc4"}
{:conform, "~> 1.0.0-rc4"},
{:slugger, github: "h4cc/slugger", ref: "558e435c232034551123f034f6dd7f1f395e6759"}
]
end
end

View file

@ -1,18 +1,20 @@
%{"bbmustache": {:hex, :bbmustache, "1.0.3"},
%{"bbmustache": {:hex, :bbmustache, "1.0.4"},
"cf": {:hex, :cf, "0.2.1"},
"conform": {:hex, :conform, "1.0.0-rc8"},
"cowboy": {:hex, :cowboy, "1.0.4"},
"cowlib": {:hex, :cowlib, "1.0.2"},
"earmark": {:hex, :earmark, "0.2.1"},
"erlware_commons": {:hex, :erlware_commons, "0.15.0"},
"exrm": {:hex, :exrm, "1.0.0-rc7"},
"erlware_commons": {:hex, :erlware_commons, "0.18.0"},
"exrm": {:hex, :exrm, "1.0.0-rc8"},
"fs": {:hex, :fs, "0.9.2"},
"getopt": {:hex, :getopt, "0.8.2"},
"neotoma": {:hex, :neotoma, "1.7.3"},
"phoenix": {:hex, :phoenix, "1.1.4"},
"phoenix_html": {:hex, :phoenix_html, "2.5.0"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.3"},
"plug": {:hex, :plug, "1.1.0"},
"poison": {:hex, :poison, "2.0.1"},
"providers": {:hex, :providers, "1.4.1"},
"plug": {:hex, :plug, "1.1.1"},
"poison": {:hex, :poison, "2.1.0"},
"providers": {:hex, :providers, "1.6.0"},
"ranch": {:hex, :ranch, "1.2.1"},
"relx": {:hex, :relx, "3.5.0"}}
"relx": {:hex, :relx, "3.16.0"},
"slugger": {:git, "https://github.com/h4cc/slugger.git", "558e435c232034551123f034f6dd7f1f395e6759", [ref: "558e435c232034551123f034f6dd7f1f395e6759"]}}

View file

@ -26,6 +26,8 @@ defmodule MebeWeb.ControllerUtils do
|> assign(:blog_name, Utils.get_conf(:blog_name))
|> assign(:blog_author, Utils.get_conf(:blog_author))
|> assign(:absolute_url, Utils.get_conf(:absolute_url))
|> assign(:multi_author_mode, Utils.get_conf(:multi_author_mode))
|> assign(:use_default_author, Utils.get_conf(:use_default_author))
|> assign(:posts_per_page, Utils.get_conf(:posts_per_page))
|> assign(:disqus_comments, Utils.get_conf(:disqus_comments))
|> assign(:page_commenting, Utils.get_conf(:page_commenting))

View file

@ -11,28 +11,34 @@ defmodule MebeWeb.FeedController do
plug :put_layout, "feed.xml"
def index(conn, params) do
posts = DB.get_reg_posts 0, Utils.get_conf(:posts_in_feed)
posts = DB.get_reg_posts(0, Utils.get_conf(:posts_in_feed))
conn
|> render_posts(posts, "postlist.xml", params)
end
def tag(conn, params) do
%{"tag" => tag} = params
posts = DB.get_tag_posts tag, 0, Utils.get_conf(:posts_in_feed)
def tag(conn, params = %{"tag" => tag}) do
posts = DB.get_tag_posts(tag, 0, Utils.get_conf(:posts_in_feed))
conn
|> assign(:tag, tag)
|> render_posts(posts, "postlist.xml", params)
end
def author(conn, params = %{"author" => author}) do
posts = DB.get_author_posts(author, 0, Utils.get_conf(:posts_in_feed))
conn
|> assign(:author, author)
|> render_posts(posts, "postlist.xml", params)
end
# A private plug to check if feeds are enabled at runtime. We need to do this
# at runtime to make it work with releases (where the release is compiled with
# configuration that was active when the release was made). This plug will halt
# the request processing, returning a 404 error if feeds are not enabled.
defp check_feeds_enabled(conn, _opts) do
case Utils.get_conf :enable_feeds do
case Utils.get_conf(:enable_feeds) do
true -> conn
false ->
conn

View file

@ -19,21 +19,37 @@ defmodule MebeWeb.PageController do
)
end
def tag(conn, params) do
%{"tag" => tag} = params
def tag(conn, params = %{"tag" => tag}) do
conn
|> assign(:tag, tag)
|> render_page(
:tag,
[tag],
fn first, last -> DB.get_tag_posts(tag, first, last) end,
DB.get_count(tag),
DB.get_count(:tag, tag),
"tag.html",
params
)
end
def author(conn, params = %{"author" => author}) do
case Utils.get_conf(:multi_author_mode) do
true ->
conn
|> assign(:author, author)
|> render_page(
:author,
[author],
fn first, last -> DB.get_author_posts(author, first, last) end,
DB.get_count(:author, author),
"author.html",
params
)
_ -> render_404(conn)
end
end
def year(conn, params) do
%{"year" => year} = params
@ -45,13 +61,13 @@ defmodule MebeWeb.PageController do
:year,
[year],
fn first, last -> DB.get_year_posts(year, first, last) end,
DB.get_count(year),
DB.get_count(:year, year),
"year.html",
params
)
_ ->
render_404 conn
render_404(conn)
end
end
@ -70,7 +86,7 @@ defmodule MebeWeb.PageController do
:month,
[year, month],
fn first, last -> DB.get_month_posts(year, month, first, last) end,
DB.get_count({year, month}),
DB.get_count(:month, {year, month}),
"month.html",
params
)
@ -126,8 +142,8 @@ defmodule MebeWeb.PageController do
# posts when called, or an empty list if none are found. The function gets
# the first and last post to display as integer arguments.
defp render_page(conn, page_type, page_args, postgetter, total_count, template, params) do
page = get_page params
{first, last} = calculate_ranges page
page = get_page(params)
{first, last} = calculate_ranges(page)
case postgetter.(first, last) do
[] ->

View file

@ -20,6 +20,10 @@ defmodule MebeWeb.Router do
get "/tag/:tag", PageController, :tag
get "/tag/:tag/feed", FeedController, :tag
get "/author/:author/p/:page", PageController, :author
get "/author/:author", PageController, :author
get "/author/:author/feed", FeedController, :author
get "/archive/:year/p/:page", PageController, :year
get "/archive/:year", PageController, :year
get "/archive/:year/:month/p/:page", PageController, :month

View file

@ -23,6 +23,11 @@
<title>
<![CDATA[<%= post.title %>]]>
</title>
<%= if @multi_author_mode do %>
<dc:creator><%= get_author(post) %></dc:creator>
<% end %>
<link><%= @absolute_url %><%= page_path @conn, :post, year, j_month, j_day, post.slug %></link>
<guid><%= @absolute_url %><%= page_path @conn, :post, year, j_month, j_day, post.slug %></guid>
<description>

View file

@ -1,9 +1,10 @@
<rss version="2.0">
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title><%= title @conn %></title>
<link><%= @absolute_url %></link>
<description></description>
<generator>Mebe</generator>
<copyright>© <%= @blog_author %></copyright>
<%= render @view_module, @view_template, assigns %>
</channel>

View file

@ -0,0 +1,7 @@
<div class="jumbotron">
<p>
Viewing posts by author <em><%= unslugify_author(@author) %></em>.
</p>
</div>
<%= render "postlist.html", assigns %>

View file

@ -6,4 +6,4 @@
</p>
</div>
<%= render "postlist.html", assigns %>
<%= render "postlist.html", assigns %>

View file

@ -8,6 +8,10 @@
}
%>
<%
author = get_author(@post)
%>
<div class="post">
<div class="post-header">
@ -22,7 +26,10 @@
<div class="post-meta">
<span>
Posted on
<a href="<%= page_path @conn, :year, year, 1 %>"><%= year %>-<%= j_month %>-<%= j_day %></a>.
<a href="<%= page_path @conn, :year, year, 1 %>"><%= year %>-<%= j_month %>-<%= j_day %></a><!--
--><%= if @multi_author_mode and author != nil do %><!--
--> by <a href="<%= page_path(@conn, :author, slugify(get_author(@post)), 1) %>"><%= get_author(@post) %></a><!--
--><% end %>.
</span>
<%= if @post.tags do %>

View file

@ -4,4 +4,4 @@
</p>
</div>
<%= render "postlist.html", assigns %>
<%= render "postlist.html", assigns %>

View file

@ -4,4 +4,4 @@
</p>
</div>
<%= render "postlist.html", assigns %>
<%= render "postlist.html", assigns %>

View file

@ -43,14 +43,14 @@ defmodule MebeWeb.Web do
use Phoenix.View, root: root
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 2]
# Import URL helpers from the router
import MebeWeb.Router.Helpers
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
# Custom helpers
import MebeWeb.Utils, only: [get_author: 1, slugify: 1, unslugify_author: 1]
end
end